mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-12 18:31:18 +00:00
feat: add an Image gizmo for zero-displacement colour
A separate toolbar tool for the simple case of texture displacement: put a colour picture on the model with no relief at all. It is a thin wrapper, not a fork. GLGizmoImage subclasses GLGizmoTextureDisplacement and overrides one new protected virtual, image_mode(). Painting, the layer stack, the paint masks, the texture library, the previews and the bake are the same code reached the same way; a zero-depth bake refines the painted area and writes the per-triangle filament indices exactly as a relief bake does, it just displaces by nothing. In the base gizmo, image_mode() does three things: - apply_image_mode_presets() pins every layer to depth 0 and turns colour on wherever the image has any, alongside the existing Standard-mode presets. - The panel leaves out Depth, Midlevel, Invert, Edge fade and Blend, and the Standard/Pro switch, which is what keeps the mesh-preparation sections hidden. Bake is disabled when no image carries colour, since that bake would refine the mesh and produce nothing. - add_texture_layer() seeds a new layer from the first library texture that has colour, rather than the first one, because most of the shipped library is grayscale height maps. The two tools share one layer stack, so opening the Image tool on a volume whose layers carry relief flattens them. Only depth changes: the paint, the image, the tiling, the mapping and any relief already baked into the mesh are untouched. The toolbar icon follows Orca's menu-icon style: 16 px, 1 px #949494 frame, #009688 on the picture inside. The shortcut is registered but unbound: every single letter the Plater context can reach is already taken, and a default that shadows one of them is worse than none. docs/HLSD/keyboard-shortcuts.md now documents that an empty default chord is a supported choice. Measured with a probe built against this tree in the texture-bake harness: a depth-0 bake of a painted cube face leaves every output vertex within 7 nm of the original surface (tolerance 1 um) while colouring 17090 of 24226 output triangles across 3 filaments; the same scene at depth 0.4 mm moves vertices up to 0.216 mm.
This commit is contained in:
@@ -124,7 +124,9 @@ Control on the Global page, and from the `?` key on the page of the view that re
|
||||
the dialog should list it; the row's section heading is the `section_table` entry
|
||||
above it, so a new section needs an entry there too. Pick a default that does not
|
||||
collide inside its contexts; the `[Shortcuts]` tests check every default against the
|
||||
others.
|
||||
others. A default `KeyChord{}` leaves the action unbound: it is listed in the dialog
|
||||
for the user to bind and matches nothing until they do, which is the honest answer
|
||||
when every key its contexts can reach is already spoken for.
|
||||
2. Handle it in the dispatcher of its context: `MainFrame::handle_global_shortcut`,
|
||||
`GLCanvas3D::handle_shortcut`, `ObjectList::dispatch_shortcut`, or a gizmo's
|
||||
`on_tool_shortcut`. A gizmo that opens on a key sets `m_shortcut` in its constructor.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><circle cx="5.4" cy="6.2" r="1.4" style="fill:#009688"/><path d="M3 12.2 6.3 7.6 8.9 10.9 10.7 8.6 13 12.2 Z" style="fill:#009688"/><rect x="1.5" y="2.5" width="13" height="11" rx="1.5" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/></svg>
|
||||
|
After Width: | Height: | Size: 353 B |
@@ -186,6 +186,8 @@ set(SLIC3R_GUI_SOURCES
|
||||
GUI/Gizmos/GLGizmoFlatten.hpp
|
||||
GUI/Gizmos/GLGizmoFuzzySkin.cpp
|
||||
GUI/Gizmos/GLGizmoFuzzySkin.hpp
|
||||
GUI/Gizmos/GLGizmoImage.cpp
|
||||
GUI/Gizmos/GLGizmoImage.hpp
|
||||
GUI/Gizmos/GLGizmoMeasure.cpp
|
||||
GUI/Gizmos/GLGizmoMeasure.hpp
|
||||
GUI/Gizmos/GLGizmoMeshBoolean.cpp
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#include "GLGizmoImage.hpp"
|
||||
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
#include "slic3r/GUI/Shortcuts.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
GLGizmoImage::GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id)
|
||||
: GLGizmoTextureDisplacement(parent, icon_filename, sprite_id)
|
||||
{
|
||||
}
|
||||
|
||||
bool GLGizmoImage::on_init()
|
||||
{
|
||||
if (!GLGizmoTextureDisplacement::on_init())
|
||||
return false;
|
||||
// Everything the base set up stays - the brush descriptions and the shortcut lists are the same
|
||||
// tools - except the key that opens it, which is this tool's own.
|
||||
m_shortcut = Shortcut::GizmoImage;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string GLGizmoImage::on_get_name() const
|
||||
{
|
||||
return _u8L("Image");
|
||||
}
|
||||
|
||||
wxString GLGizmoImage::handle_snapshot_action_name(bool shift_down, GLGizmoPainterBase::Button button_down) const
|
||||
{
|
||||
// Same paint, same masks; only the undo step's wording follows the tool the user is in.
|
||||
const bool erasing = shift_down || (m_erase_mode != (button_down == GLGizmoPainterBase::Button::Right));
|
||||
return erasing ? _L("Erase image paint") : _L("Paint image");
|
||||
}
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef slic3r_GLGizmoImage_hpp_
|
||||
#define slic3r_GLGizmoImage_hpp_
|
||||
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/I18N.hpp"
|
||||
|
||||
#include <string>
|
||||
#include <wx/string.h>
|
||||
|
||||
namespace Slic3r::GUI {
|
||||
|
||||
class GLCanvas3D;
|
||||
|
||||
// "Put this picture on the model": the texture-displacement gizmo with the relief taken out of it.
|
||||
//
|
||||
// There is no second engine here and no copy of one. It is GLGizmoTextureDisplacement with
|
||||
// image_mode() on, which pins every layer to zero depth and colour on (see
|
||||
// apply_image_mode_presets()) and leaves out the controls that only mean something once the surface
|
||||
// moves. Painting, the layer stack, the texture library, the previews and the bake are the very same
|
||||
// code, reached the very same way - a zero-depth bake refines the painted area and writes the
|
||||
// colours exactly as a relief bake does, it just displaces by nothing.
|
||||
//
|
||||
// Its own toolbar button because the two are different jobs, not two settings of one: a user who
|
||||
// wants a photo on a part should not have to know what a height map is, or find the one slider that
|
||||
// has to be zero.
|
||||
class GLGizmoImage : public GLGizmoTextureDisplacement
|
||||
{
|
||||
public:
|
||||
GLGizmoImage(GLCanvas3D &parent, const std::string &icon_filename, unsigned int sprite_id);
|
||||
|
||||
protected:
|
||||
bool image_mode() const override { return true; }
|
||||
|
||||
bool on_init() override;
|
||||
std::string on_get_name() const override;
|
||||
|
||||
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
|
||||
|
||||
std::string get_gizmo_entering_text() const override { return _u8L("Entering Image painting"); }
|
||||
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Image painting"); }
|
||||
std::string get_action_snapshot_name() const override { return _u8L("Image editing"); }
|
||||
};
|
||||
|
||||
} // namespace Slic3r::GUI
|
||||
|
||||
#endif // slic3r_GLGizmoImage_hpp_
|
||||
@@ -3723,16 +3723,29 @@ void GLGizmoTextureDisplacement::add_texture_layer()
|
||||
|
||||
TextureDisplacementLayer layer;
|
||||
layer.slot = free_slot;
|
||||
// Start the layer off on the first library texture rather than on nothing at all: a textureless
|
||||
// layer looks broken (painting on it appears to do nothing, because there is no height map to
|
||||
// displace by). The user swaps it for another from the layer's own picker.
|
||||
// Start the layer off on a library texture rather than on nothing at all: a textureless layer
|
||||
// looks broken (painting on it appears to do nothing, because there is no height map to displace
|
||||
// by). The user swaps it for another from the layer's own picker.
|
||||
//
|
||||
// The Image tool needs that first texture to have colour in it, since colour is the only thing it
|
||||
// bakes - most of the shipped library is grayscale height maps, and landing on one of those would
|
||||
// open the tool on an image it then warns about.
|
||||
const std::vector<TextureLibraryEntry> &library = texture_library();
|
||||
if (!library.empty())
|
||||
if (const LibraryTexture *tex = get_library_texture(library.front().path)) {
|
||||
layer.name = library.front().name;
|
||||
layer.path = library.front().path;
|
||||
layer.image_data = tex->image_data;
|
||||
for (const TextureLibraryEntry &entry : library) {
|
||||
const LibraryTexture *tex = get_library_texture(entry.path);
|
||||
if (tex == nullptr)
|
||||
continue;
|
||||
if (image_mode()) {
|
||||
TextureDisplacementLayer probe;
|
||||
probe.image_data = tex->image_data;
|
||||
if (!height_texture_has_color(probe))
|
||||
continue;
|
||||
}
|
||||
layer.name = entry.name;
|
||||
layer.path = entry.path;
|
||||
layer.image_data = tex->image_data;
|
||||
break;
|
||||
}
|
||||
mv->texture_displacement_layers.push_back(std::move(layer));
|
||||
// A new layer opens with every setting showing; there is nothing on it yet to hide settings from.
|
||||
m_layer_expanded[size_t(free_slot)] = true;
|
||||
@@ -5442,6 +5455,40 @@ bool GLGizmoTextureDisplacement::apply_standard_mode_presets(ModelVolume *mv)
|
||||
return changed;
|
||||
}
|
||||
|
||||
// Pinned into the volume rather than applied on the way to the bake, exactly as Standard mode pins
|
||||
// displace_border and smooth_enabled: the preview, the bake, the triangle estimate and the UV pane all
|
||||
// read the layers straight off the volume, and a value that only some of them saw would put the
|
||||
// preview and the bake out of step.
|
||||
//
|
||||
// The two tools share one layer stack - that is what "not a fork" means here - so opening the Image
|
||||
// tool on a volume whose layers carry relief flattens them, and they stay flat when the displacement
|
||||
// tool is reopened. Only that one number changes: the paint, the image, the tiling, the mapping and
|
||||
// any relief already baked into the mesh are untouched.
|
||||
bool GLGizmoTextureDisplacement::apply_image_mode_presets(ModelVolume *mv)
|
||||
{
|
||||
if (mv == nullptr)
|
||||
return false;
|
||||
bool changed = false;
|
||||
for (TextureDisplacementLayer &layer : mv->texture_displacement_layers) {
|
||||
// Zero depth is the whole of "no relief": the bake still refines the painted area and still
|
||||
// writes the colours, every layer just contributes a displacement of 0 (see
|
||||
// make_combined_displacement_sampler()). Nothing special-cases it.
|
||||
if (layer.depth_mm != 0.f) {
|
||||
layer.depth_mm = 0.f;
|
||||
changed = true;
|
||||
}
|
||||
// Colour is the only thing an image bake produces, so it is on wherever there is colour to
|
||||
// use rather than being a checkbox. A grayscale picture leaves it off, which is what makes
|
||||
// the panel's "this image has no colors" note the honest answer instead of a silent no-op.
|
||||
const bool wanted = height_texture_has_color(layer);
|
||||
if (layer.color_enabled != wanted) {
|
||||
layer.color_enabled = wanted;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
return changed;
|
||||
}
|
||||
|
||||
void GLGizmoTextureDisplacement::bake_standard()
|
||||
{
|
||||
ModelVolume *mv = texture_volume();
|
||||
@@ -5798,9 +5845,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
const float previous_body_h = m_panel_body_h;
|
||||
const float previous_footer_h = m_panel_footer_h;
|
||||
|
||||
// The Image tool is Standard with the relief taken out of it; there is no Pro there to switch to.
|
||||
if (image_mode())
|
||||
m_panel_mode = 0;
|
||||
// Pinned every frame while Standard is active, so what Preview shows is always what Bake will do.
|
||||
if (!pro_mode() && apply_standard_mode_presets(mv))
|
||||
m_preview_params_dirty = true;
|
||||
if (image_mode() && apply_image_mode_presets(mv))
|
||||
m_preview_params_dirty = true;
|
||||
|
||||
// Layout follows the Option C design: a fixed-width panel, a header pinned at the top, a body that
|
||||
// scrolls as one, and a footer with Bake pinned at the bottom. Rows are laid out against panel_w and
|
||||
@@ -5955,21 +6007,26 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
{
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
heading(_L("Texture displacement"));
|
||||
// The tool's own name, so the Image panel does not announce itself as the one it is built on.
|
||||
heading(wxString::FromUTF8(get_name(false)));
|
||||
|
||||
const std::string labels[2] = { _u8L("Standard"), _u8L("Pro") };
|
||||
const float seg_pad = m_imgui->scaled(0.5f);
|
||||
const float seg_w[2] = { ImGui::CalcTextSize(labels[0].c_str()).x + 2.f * seg_pad, ImGui::CalcTextSize(labels[1].c_str()).x + 2.f * seg_pad };
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + panel_w - (seg_w[0] + seg_w[1] + gap_s + icon_sm)));
|
||||
|
||||
// Standard / Pro is a mode, not an option: Standard hides every mesh-preparation control and folds
|
||||
// the whole recipe into Bake, Pro shows all of it and hands the ordering to the user.
|
||||
// the whole recipe into Bake, Pro shows all of it and hands the ordering to the user. The Image
|
||||
// tool is the restricted case of Standard, so it offers neither and the segment is left out.
|
||||
const int mode_buttons = image_mode() ? 0 : 2;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(),
|
||||
x0 + panel_w - icon_sm -
|
||||
(mode_buttons > 0 ? seg_w[0] + seg_w[1] + gap_s : 0.f)));
|
||||
|
||||
const ImVec2 seg_min = ImGui::GetCursorScreenPos();
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0.f, style.ItemSpacing.y));
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, 0.f);
|
||||
m_imgui->disabled_begin(busy);
|
||||
for (int m = 0; m < 2; ++m) {
|
||||
for (int m = 0; m < mode_buttons; ++m) {
|
||||
if (m > 0)
|
||||
ImGui::SameLine();
|
||||
const bool on = m_panel_mode == m;
|
||||
@@ -6000,10 +6057,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::GetWindowDrawList()->AddRect(seg_min, ImVec2(ImGui::GetItemRectMax().x, seg_min.y + frame_h),
|
||||
ImGui::GetColorU32(col_frame), style.FrameRounding);
|
||||
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
if (mode_buttons > 0) {
|
||||
ImGui::GetWindowDrawList()->AddRect(seg_min, ImVec2(ImGui::GetItemRectMax().x, seg_min.y + frame_h),
|
||||
ImGui::GetColorU32(col_frame), style.FrameRounding);
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
}
|
||||
if (icon_button(806, "canvas_drag.svg", icon_sm, m_undocked ? _L("Dock panel") : _L("Undock panel"),
|
||||
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside "
|
||||
"the toolbar.")))
|
||||
@@ -6276,7 +6334,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
// ---- Texture layers: the active layer as an open card, every other one as a single row ----
|
||||
{
|
||||
ImGui::AlignTextToFramePadding();
|
||||
heading(_L("Texture layers"));
|
||||
heading(image_mode() ? _L("Images") : _L("Texture layers"));
|
||||
if (mv != nullptr) {
|
||||
const std::string count = std::to_string(mv->texture_displacement_layers.size()) + " / " +
|
||||
std::to_string(TEXTURE_DISPLACEMENT_MAX_LAYERS);
|
||||
@@ -6451,9 +6509,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
// The three controls nearly every layer needs; everything else waits behind "More settings".
|
||||
// Depth and tile size are logarithmic, so both ends of their wide ranges stay usable.
|
||||
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad);
|
||||
hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in "
|
||||
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
|
||||
// Depth is the one the Image tool has no use for: it is pinned to 0 there, and a slider
|
||||
// that must not be moved is worse than none.
|
||||
if (!image_mode()) {
|
||||
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad);
|
||||
hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in "
|
||||
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
|
||||
}
|
||||
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad);
|
||||
hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
|
||||
"more often and makes its detail finer; with Tile off, this is the size of the "
|
||||
@@ -6474,65 +6536,87 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
|
||||
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards;
|
||||
// at 0.5 mid-grey is neutral and darker texels cut inwards.
|
||||
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad);
|
||||
hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes "
|
||||
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
|
||||
"still push out - one image both embosses and engraves.\n\n"
|
||||
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
|
||||
"cut can pass through the other side."));
|
||||
if (layer.midlevel > 0.f && layer.depth_mm > 1.f) {
|
||||
ImGui::PushTextWrapPos(content_rx - card_pad);
|
||||
m_imgui->warning_text(_L("Deep inward displacement may self-intersect."));
|
||||
ImGui::PopTextWrapPos();
|
||||
// at 0.5 mid-grey is neutral and darker texels cut inwards. Every setting from here to
|
||||
// Smoothing describes relief only, so the Image tool leaves the lot out.
|
||||
if (!image_mode()) {
|
||||
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad);
|
||||
hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes "
|
||||
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
|
||||
"still push out - one image both embosses and engraves.\n\n"
|
||||
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
|
||||
"cut can pass through the other side."));
|
||||
if (layer.midlevel > 0.f && layer.depth_mm > 1.f) {
|
||||
ImGui::PushTextWrapPos(content_rx - card_pad);
|
||||
m_imgui->warning_text(_L("Deep inward displacement may self-intersect."));
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
}
|
||||
|
||||
// Blurs the image itself, so it is as much a colour control as a relief one - the Image
|
||||
// tool keeps it for exactly that.
|
||||
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad);
|
||||
hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes "
|
||||
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
|
||||
"costs fine detail."));
|
||||
|
||||
// Edge fade: the relief flattens toward the boundary of the painted area.
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing);
|
||||
hover_tip(_u8L("Flattens the relief as it approaches the edge of the painted area, so it "
|
||||
"blends into the bare surface instead of stopping at a step."));
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(!layer.edge_smoothing);
|
||||
ImGui::SetNextItemWidth(-card_pad);
|
||||
m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f",
|
||||
ImGuiSliderFlags_AlwaysClamp);
|
||||
m_imgui->disabled_end();
|
||||
hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values "
|
||||
"soften a narrow band at the edge; 1 flattens almost all of it."));
|
||||
if (!image_mode()) {
|
||||
// Edge fade: the relief flattens toward the boundary of the painted area.
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing);
|
||||
hover_tip(_u8L("Flattens the relief as it approaches the edge of the painted area, so it "
|
||||
"blends into the bare surface instead of stopping at a step."));
|
||||
ImGui::SameLine();
|
||||
m_imgui->disabled_begin(!layer.edge_smoothing);
|
||||
ImGui::SetNextItemWidth(-card_pad);
|
||||
m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f",
|
||||
ImGuiSliderFlags_AlwaysClamp);
|
||||
m_imgui->disabled_end();
|
||||
hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values "
|
||||
"soften a narrow band at the edge; 1 flattens almost all of it."));
|
||||
}
|
||||
|
||||
// Invert and Colours share a row.
|
||||
{
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Invert"), layer.invert);
|
||||
hover_tip(_u8L("Turns the relief inside out: what stood out is cut in, and the other way "
|
||||
"round. The same as using a negative of the image."));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + 0.5f * (ImGui::GetWindowContentRegionMax().x - card_pad - x0)));
|
||||
if (!image_mode()) {
|
||||
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Invert"), layer.invert);
|
||||
hover_tip(_u8L("Turns the relief inside out: what stood out is cut in, and the other way "
|
||||
"round. The same as using a negative of the image."));
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + 0.5f * (ImGui::GetWindowContentRegionMax().x - card_pad - x0)));
|
||||
}
|
||||
|
||||
// Only offered for a texture that actually has colour - the shipped library is grayscale,
|
||||
// and a checkbox that silently does nothing on nine textures out of ten is worse than no
|
||||
// checkbox. Disabled rather than hidden so it is clear the feature exists and what it wants.
|
||||
//
|
||||
// In the Image tool colour is not a choice at all - it is the only thing the bake produces -
|
||||
// so the checkbox gives way to a warning on the one case that cannot work: a picture with no
|
||||
// colour in it, which would bake to nothing.
|
||||
const bool has_color = decode_height_texture(layer).has_color();
|
||||
bool color_enabled = layer.color_enabled && has_color;
|
||||
m_imgui->disabled_begin(!has_color);
|
||||
if (m_imgui->bbl_checkbox(_L("Colors"), color_enabled)) {
|
||||
layer.color_enabled = color_enabled;
|
||||
m_preview_params_dirty = true;
|
||||
const bool color_enabled = image_mode() ? has_color : (layer.color_enabled && has_color);
|
||||
if (image_mode()) {
|
||||
if (!has_color) {
|
||||
ImGui::PushTextWrapPos(content_rx - card_pad);
|
||||
m_imgui->warning_text(_L("This image is grayscale - pick one with colors in it."));
|
||||
ImGui::PopTextWrapPos();
|
||||
}
|
||||
} else {
|
||||
bool enable = color_enabled;
|
||||
m_imgui->disabled_begin(!has_color);
|
||||
if (m_imgui->bbl_checkbox(_L("Colors"), enable)) {
|
||||
layer.color_enabled = enable;
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(has_color ?
|
||||
_u8L("Prints the painted area in the texture's colors as well as its "
|
||||
"relief. Each color is matched to the nearest of your loaded "
|
||||
"filaments; anything you did not paint keeps the object's own.") :
|
||||
_u8L("This texture is a grayscale height map, so it has no colors to "
|
||||
"apply. Import a color image to use this."),
|
||||
wrap_w);
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(has_color ?
|
||||
_u8L("Prints the painted area in the texture's colors as well as its "
|
||||
"relief. Each color is matched to the nearest of your loaded "
|
||||
"filaments; anything you did not paint keeps the object's own.") :
|
||||
_u8L("This texture is a grayscale height map, so it has no colors to "
|
||||
"apply. Import a color image to use this."),
|
||||
wrap_w);
|
||||
|
||||
// The rest of colour belongs to the whole stack, not to this layer, so it only appears once -
|
||||
// under whichever layer turned colour on.
|
||||
@@ -6720,40 +6804,51 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
}
|
||||
}
|
||||
|
||||
// Blend, and placement on the model.
|
||||
// Blend, and placement on the model. Blend combines *displacements*, so with every
|
||||
// layer pinned to zero depth it has nothing to combine - the Image tool shows only the
|
||||
// placement handle. Colour does not blend either way: a higher layer simply paints over
|
||||
// a lower one where they overlap.
|
||||
{
|
||||
const float x0 = ImGui::GetCursorPosX();
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextDisabled("%s", _u8L("Blend").c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement");
|
||||
const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement");
|
||||
const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement");
|
||||
const std::string blend_divide = _u8L("Divide");
|
||||
const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() };
|
||||
// The first layer has nothing before it to combine with - build_texture_displacement() makes it
|
||||
// add regardless - so its combo shows Add and cannot be changed.
|
||||
const bool base_layer = li == 0;
|
||||
int blend_mode = base_layer ? 0 : static_cast<int>(layer.blend_mode);
|
||||
ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm));
|
||||
m_imgui->disabled_begin(base_layer);
|
||||
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) {
|
||||
layer.blend_mode = static_cast<TextureBlendMode>(blend_mode);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(base_layer ?
|
||||
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
|
||||
_u8L("What this layer does where it overlaps the ones below. Add and "
|
||||
"Subtract pile relief on or carve it away. Multiply and Divide scale "
|
||||
"what is underneath, which turns this layer into a mask over it - "
|
||||
"there, Depth acts as a strength, and 1 mm leaves white areas "
|
||||
"untouched."),
|
||||
wrap_w);
|
||||
if (!image_mode()) {
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextDisabled("%s", _u8L("Blend").c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement");
|
||||
const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement");
|
||||
const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement");
|
||||
const std::string blend_divide = _u8L("Divide");
|
||||
const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() };
|
||||
// The first layer has nothing before it to combine with - build_texture_displacement() makes it
|
||||
// add regardless - so its combo shows Add and cannot be changed.
|
||||
const bool base_layer = li == 0;
|
||||
int blend_mode = base_layer ? 0 : static_cast<int>(layer.blend_mode);
|
||||
ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm));
|
||||
m_imgui->disabled_begin(base_layer);
|
||||
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) {
|
||||
layer.blend_mode = static_cast<TextureBlendMode>(blend_mode);
|
||||
m_preview_params_dirty = true;
|
||||
}
|
||||
m_imgui->disabled_end();
|
||||
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
||||
m_imgui->tooltip(base_layer ?
|
||||
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
|
||||
_u8L("What this layer does where it overlaps the ones below. Add and "
|
||||
"Subtract pile relief on or carve it away. Multiply and Divide scale "
|
||||
"what is underneath, which turns this layer into a mask over it - "
|
||||
"there, Depth acts as a strength, and 1 mm leaves white areas "
|
||||
"untouched."),
|
||||
wrap_w);
|
||||
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
ImGui::SameLine(0.f, gap_s);
|
||||
} else {
|
||||
// On its own, the handle toggle needs the label the Blend row would have carried.
|
||||
ImGui::AlignTextToFramePadding();
|
||||
ImGui::TextDisabled("%s", _u8L("Placement").c_str());
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
||||
}
|
||||
if (icon_toggle(842, "texture_displacement_adjust.svg", m_adjust_texture_mode, icon_sm, _L("Adjust placement"),
|
||||
_L("Adjust placement - drag the handle on the model to move the texture"))) {
|
||||
if (!m_adjust_texture_mode) {
|
||||
@@ -6847,9 +6942,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(_u8L("Pick a different image for this layer. The paint, depth and tiling stay as they are."));
|
||||
ImGui::SameLine();
|
||||
|
||||
// Depth is what a closed relief layer is summarised by; with it pinned to zero that says
|
||||
// nothing, so the Image tool reports the one state a closed row still has.
|
||||
char depth_text[32];
|
||||
std::snprintf(depth_text, sizeof(depth_text), "%.3f mm", layer.depth_mm);
|
||||
const std::string summary = painted ? std::string(depth_text) : _u8L("not painted");
|
||||
const std::string summary = !painted ? _u8L("not painted") :
|
||||
image_mode() ? _u8L("painted") :
|
||||
std::string(depth_text);
|
||||
const float sum_w = ImGui::CalcTextSize(summary.c_str()).x;
|
||||
const float right_edge = ImGui::GetWindowContentRegionMax().x - inner;
|
||||
const float name_w = std::max(0.f, right_edge - ImGui::GetCursorPosX() - 2.f * gap_s - sum_w - frame_h);
|
||||
@@ -6903,7 +7002,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
char depth_text[32];
|
||||
std::snprintf(depth_text, sizeof(depth_text), "%.3f mm", l.depth_mm);
|
||||
const std::string summary = slot_painted(l.slot) ? std::string(depth_text) : _u8L("not painted");
|
||||
const std::string summary = !slot_painted(l.slot) ? _u8L("not painted") :
|
||||
image_mode() ? _u8L("painted") :
|
||||
std::string(depth_text);
|
||||
const float sum_w = ImGui::CalcTextSize(summary.c_str()).x;
|
||||
const float text_y = mn.y + 0.5f * (row_h - ImGui::GetFontSize());
|
||||
fg->AddText(ImVec2(mx.x - inner - sum_w, text_y), ImGui::GetColorU32(ImGuiCol_TextDisabled), summary.c_str());
|
||||
@@ -6914,8 +7015,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
if (mv != nullptr) {
|
||||
const bool full = mv->texture_displacement_layers.size() >= TEXTURE_DISPLACEMENT_MAX_LAYERS;
|
||||
const std::string label = full ? Slic3r::format(_u8L("All %1% layers used"), TEXTURE_DISPLACEMENT_MAX_LAYERS) :
|
||||
"+ " + _u8L("Add layer");
|
||||
const std::string label = full ? Slic3r::format(_u8L("All %1% layers used"), TEXTURE_DISPLACEMENT_MAX_LAYERS) :
|
||||
image_mode() ? "+ " + _u8L("Add image") :
|
||||
"+ " + _u8L("Add layer");
|
||||
m_imgui->disabled_begin(busy || full);
|
||||
if (ImGui::Button((label + "##add_layer").c_str(), ImVec2(ImGui::GetContentRegionAvail().x, 0.f)))
|
||||
add_texture_layer();
|
||||
@@ -7305,6 +7407,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
|
||||
"budget %2% k. Untick Auto to set them yourself."),
|
||||
rec.edge_mm, rec.budget_k));
|
||||
else if (image_mode())
|
||||
// Same number, different thing to look at: with no relief to carry, the mesh under the
|
||||
// paint is only ever as fine as the color edges printed along it need it to be.
|
||||
hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. Each triangle "
|
||||
"prints in one filament, so this is how sharp an edge between two colors "
|
||||
"can come out. Smaller costs triangles fast: halving it needs four times "
|
||||
"as many, and once they no longer fit the budget the bake simplifies back "
|
||||
"down and the sharpness goes with it."));
|
||||
else
|
||||
hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be "
|
||||
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
|
||||
@@ -7385,7 +7495,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
|
||||
ImGui::SameLine(x0 + panel_w - button_w * 2 - style.ItemSpacing.x);
|
||||
|
||||
const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted();
|
||||
// Colour is the only thing an image bake produces, so with no colouring layer there would be
|
||||
// nothing to show for it - and it is not a cheap no-op: the bake would still refine the painted
|
||||
// area and replace the mesh with the result.
|
||||
const bool no_colors = image_mode() && mv != nullptr && !any_layer_colors(*mv);
|
||||
const bool can_bake = !busy && mv != nullptr && mv->is_texture_displacement_painted() && !no_colors;
|
||||
const std::string bake_label = m_prepare_in_progress ? _u8L("Preparing...") :
|
||||
m_bake_in_progress ? _u8L("Baking...") :
|
||||
into_u8(m_desc.at("bake"));
|
||||
@@ -7410,6 +7524,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
||||
"strokes on the model mark seams instead of painting - turn "
|
||||
"it off in the pane to paint.") :
|
||||
_u8L("Nothing is painted yet.")) :
|
||||
no_colors ?
|
||||
_u8L("None of the images have colors in them, so there is nothing to print. Pick a "
|
||||
"color picture.") :
|
||||
image_mode() ?
|
||||
_u8L("Prints the painted area in the picture's colors. Each color is matched to the "
|
||||
"nearest of your loaded filaments; the surface itself is left exactly where it "
|
||||
"is, and anything you did not paint keeps the object's own color.") :
|
||||
pro_mode() ?
|
||||
_u8L("Turn the painted height maps into real geometry, by moving the vertices that are "
|
||||
"already there. Use Subdivide first if the mesh is too coarse to show the detail.") :
|
||||
|
||||
@@ -204,6 +204,14 @@ public:
|
||||
bool on_mouse(const wxMouseEvent &mouse_event) override;
|
||||
|
||||
protected:
|
||||
// True in the Image tool (see GLGizmoImage): this same gizmo restricted to the one case where
|
||||
// the surface does not move at all - a colour picture laid on the model. Everything behind the
|
||||
// panel is shared and unchanged: the same layers, the same paint masks, the same bake. This flag
|
||||
// only tells the panel which of the two tools it is drawing, and makes it pin the values the
|
||||
// restricted case fixes (see apply_image_mode_presets()).
|
||||
virtual bool image_mode() const { return false; }
|
||||
|
||||
bool on_init() override;
|
||||
void on_render_input_window(float x, float y, float bottom_limit) override;
|
||||
std::string on_get_name() const override;
|
||||
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
|
||||
@@ -221,8 +229,11 @@ protected:
|
||||
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; }
|
||||
|
||||
// Paint / Erase, from the panel. See get_left_button_state_type(). Protected because a subclass
|
||||
// overriding handle_snapshot_action_name() has to read the same flag to name the undo step.
|
||||
bool m_erase_mode = false;
|
||||
|
||||
private:
|
||||
bool on_init() override;
|
||||
void update_model_object() override;
|
||||
void update_from_model_object(bool first_update) override;
|
||||
void on_opening() override {}
|
||||
@@ -262,6 +273,11 @@ private:
|
||||
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
|
||||
// changed something, so the caller can invalidate the preview.
|
||||
bool apply_standard_mode_presets(ModelVolume *mv);
|
||||
// Image mode's fixed values, on top of Standard's and pinned the same way (every frame, so the
|
||||
// preview cannot disagree with the bake): zero depth on every layer, and colour on wherever the
|
||||
// picture has any. Those two are what turn the displacement bake into "put this picture on the
|
||||
// model" without a second code path. Returns true if it actually changed something.
|
||||
bool apply_image_mode_presets(ModelVolume *mv);
|
||||
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
|
||||
// The order matters and is the whole reason this is one button - a height map can only move
|
||||
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
|
||||
@@ -399,8 +415,6 @@ private:
|
||||
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
|
||||
void import_custom_texture(TextureDisplacementLayer &layer);
|
||||
|
||||
// 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{};
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoImage.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
|
||||
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
|
||||
@@ -201,6 +202,11 @@ void GLGizmosManager::switch_gizmos_icon_filename()
|
||||
// light/dark switch.
|
||||
gizmo->set_icon_filename("toolbar_texture_displacement.svg");
|
||||
break;
|
||||
case(EType::Image):
|
||||
// Drawn flat in Orca's menu-icon palette, which reads on either background, so one file
|
||||
// serves both themes here too.
|
||||
gizmo->set_icon_filename("toolbar_image.svg");
|
||||
break;
|
||||
case(EType::MeshBoolean):
|
||||
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
|
||||
break;
|
||||
@@ -252,6 +258,8 @@ bool GLGizmosManager::init()
|
||||
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
|
||||
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
|
||||
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
|
||||
// The same gizmo restricted to the zero-displacement case; see GLGizmoImage.
|
||||
m_gizmos.emplace_back(new GLGizmoImage(m_parent, "toolbar_image.svg", EType::Image));
|
||||
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
|
||||
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
|
||||
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
|
||||
@@ -585,6 +593,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
|
||||
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == TextureDisplacement)
|
||||
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == Image)
|
||||
return dynamic_cast<GLGizmoImage*>(m_gizmos[Image].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == MeshBoolean)
|
||||
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
|
||||
else if (m_current == BrimEars)
|
||||
@@ -599,6 +609,7 @@ bool GLGizmosManager::is_paint_gizmo()
|
||||
m_current == EType::MmSegmentation ||
|
||||
m_current == EType::FuzzySkin ||
|
||||
m_current == EType::TextureDisplacement ||
|
||||
m_current == EType::Image ||
|
||||
m_current == EType::Seam;
|
||||
}
|
||||
|
||||
@@ -1550,6 +1561,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
|
||||
return "Fuzzy Skin Painting";
|
||||
case GLGizmosManager::EType::TextureDisplacement:
|
||||
return "Texture Displacement";
|
||||
case GLGizmosManager::EType::Image:
|
||||
return "Image";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -96,6 +96,7 @@ public:
|
||||
FuzzySkin,
|
||||
MmSegmentation,
|
||||
TextureDisplacement,
|
||||
Image,
|
||||
Emboss,
|
||||
Svg,
|
||||
Measure,
|
||||
|
||||
@@ -114,6 +114,9 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
|
||||
SHORTCUT(GizmoAssembly, "gizmo_assembly", L("Gizmo assemble"), PLATER, { 'Y' }),
|
||||
SHORTCUT(GizmoBrimEars, "gizmo_brim_ears", L("Gizmo brim ears"), PLATER, { 'E' }),
|
||||
SHORTCUT(GizmoDisplacement, "gizmo_displacement", L("Gizmo texture displacement painting"), PLATER, { 'D' }),
|
||||
// Unbound by default: every single letter this context can reach is already taken, and a default
|
||||
// that shadows one of them is worse than none. The shortcuts dialog lists it for binding.
|
||||
SHORTCUT(GizmoImage, "gizmo_image", L("Gizmo image painting"), PLATER, KeyChord{}),
|
||||
|
||||
// Sliders
|
||||
SHORTCUT(GoToLayer, "go_to_layer", L("Jump to layer"), PREVIEW, { 'G', SHIFT }),
|
||||
|
||||
@@ -38,7 +38,7 @@ enum class Shortcut : uint8_t {
|
||||
MoveSelectionLeft, MoveSelectionRight, MoveSelectionUp, MoveSelectionDown, RotateSelectionLeft, RotateSelectionRight,
|
||||
// Gizmos
|
||||
GizmoMove, GizmoRotate, GizmoScale, GizmoFlatten, GizmoCut, GizmoMeshBoolean, GizmoFdmSupports, GizmoSeam, GizmoFuzzySkin,
|
||||
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement,
|
||||
GizmoMmuSegmentation, GizmoEmboss, GizmoMeasure, GizmoAssembly, GizmoBrimEars, GizmoDisplacement, GizmoImage,
|
||||
// Sliders
|
||||
GoToLayer, LayerSliderUp, LayerSliderDown, MovesSliderLeft, MovesSliderRight, MovesSliderStart, MovesSliderEnd,
|
||||
// Painting tools
|
||||
|
||||
Reference in New Issue
Block a user