|
|
|
@@ -3830,6 +3830,189 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
|
|
|
|
|
m_panel_icon_map[names[i]] = icons[i];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Seconds on a real clock, for the tooltips' own timing.
|
|
|
|
|
//
|
|
|
|
|
// Not ImGui::GetTime(): nothing in OrcaSlicer ever sets io.DeltaTime, so it keeps imgui's default of
|
|
|
|
|
// 1/60 per frame and its clock counts frames rather than seconds. On a canvas that renders only when
|
|
|
|
|
// something asks it to, that is not a clock at all - an animation driven by it crawled while the canvas
|
|
|
|
|
// was idle and raced while a moving mouse was rendering it flat out.
|
|
|
|
|
static double tip_now()
|
|
|
|
|
{
|
|
|
|
|
using clock = std::chrono::steady_clock;
|
|
|
|
|
static const clock::time_point base = clock::now();
|
|
|
|
|
return std::chrono::duration<double>(clock::now() - base).count();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// How long the pointer has to rest on a control before its tooltip appears. The panel is dense and the
|
|
|
|
|
// tooltip lands on the model beside it, so with no wait at all a sweep from the top of the panel down to
|
|
|
|
|
// Bake flashes a dozen large panels over the scene. Once one has just been up the wait drops to almost
|
|
|
|
|
// nothing, so reading several settings in a row stays quick - but not to nothing, or that same sweep
|
|
|
|
|
// would flash again the moment one tooltip had been seen.
|
|
|
|
|
static constexpr double TIP_DELAY = 0.45;
|
|
|
|
|
static constexpr double TIP_DELAY_AGAIN = 0.10;
|
|
|
|
|
static constexpr double TIP_STICKY = 0.70;
|
|
|
|
|
|
|
|
|
|
void GLGizmoTextureDisplacement::panel_tooltip(const wxString &text, const std::vector<TipMedia> &media)
|
|
|
|
|
{
|
|
|
|
|
panel_tooltip(into_u8(text), media);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GLGizmoTextureDisplacement::panel_tooltip(const std::string &text, const std::vector<TipMedia> &media)
|
|
|
|
|
{
|
|
|
|
|
// Nothing explains a control while the control is being used: dragging a slider, typing a value
|
|
|
|
|
// into one, picking from a combo, dragging a layer card. The pointer sits on the very thing being
|
|
|
|
|
// edited, so without this the tooltip covers the model exactly while the user is watching it change.
|
|
|
|
|
// The full wait comes back afterwards rather than the short one - having just let go of a slider is
|
|
|
|
|
// not a reason to be shown a picture of it.
|
|
|
|
|
if (ImGui::IsAnyItemActive() || ImGui::IsAnyMouseDown()) {
|
|
|
|
|
m_tip_item = ImVec2(-1.f, -1.f);
|
|
|
|
|
m_tip_last_shown = -1e9;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// The item's position stands in for its identity: moving to another control restarts the wait, and
|
|
|
|
|
// staying on this one carries it on.
|
|
|
|
|
const double now = tip_now();
|
|
|
|
|
const ImVec2 item = ImGui::GetItemRectMin();
|
|
|
|
|
if (item.x != m_tip_item.x || item.y != m_tip_item.y) {
|
|
|
|
|
m_tip_item = item;
|
|
|
|
|
m_tip_hover_since = now;
|
|
|
|
|
}
|
|
|
|
|
if (now - m_tip_hover_since < (now - m_tip_last_shown < TIP_STICKY ? TIP_DELAY_AGAIN : TIP_DELAY)) {
|
|
|
|
|
// A pointer resting on a control produces no events at all, so unless the wait asks for frames
|
|
|
|
|
// itself nothing redraws the canvas and the wait never finishes - the tooltip then appears only
|
|
|
|
|
// on the next pixel of mouse movement.
|
|
|
|
|
//
|
|
|
|
|
// request_extra_frame() and not schedule_extra_frame(): the latter only arranges a timer, whose
|
|
|
|
|
// event an idle event is assumed to follow, and GLCanvas3D::on_idle() renders nothing unless the
|
|
|
|
|
// canvas was already dirty when it ran. request_extra_frame() ends on_idle with
|
|
|
|
|
// evt.RequestMore(), which obliges wx to deliver the next one. It is what 3DBed's fade uses.
|
|
|
|
|
// Costing a few hundred milliseconds of frames is the cheaper half of that trade.
|
|
|
|
|
m_parent.request_extra_frame();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
m_tip_last_shown = now;
|
|
|
|
|
|
|
|
|
|
// ImGui hides a window on the first frame it is submitted, to measure it before showing it - and
|
|
|
|
|
// for a tooltip it does so every time one reappears, because the windows are recycled (see the
|
|
|
|
|
// Popup|Tooltip branch of Begin()). So the frame on which a tooltip is decided is never the frame
|
|
|
|
|
// on which it is seen, and one that then asks for nothing more stays invisible until the next pixel
|
|
|
|
|
// of mouse movement happens to produce another frame. Keep frames coming until it has settled.
|
|
|
|
|
const int shown_frame = ImGui::GetFrameCount();
|
|
|
|
|
m_tip_settled = (shown_frame == m_tip_shown_frame + 1) ? m_tip_settled + 1 : 0;
|
|
|
|
|
m_tip_shown_frame = shown_frame;
|
|
|
|
|
if (m_tip_settled < 2)
|
|
|
|
|
m_parent.request_extra_frame();
|
|
|
|
|
|
|
|
|
|
const ImGuiStyle &style = ImGui::GetStyle();
|
|
|
|
|
const float gap = m_imgui->scaled(0.5f);
|
|
|
|
|
const float wrap = m_imgui->scaled(20.f);
|
|
|
|
|
// The width has to be known here, not left to ImGui, because the tooltip is placed by its left edge
|
|
|
|
|
// (below) and an illustration has to be laid out against something. One carrying illustrations takes
|
|
|
|
|
// the full wrap width; a plain one takes exactly what its text needs, as it did when ImGui sized it,
|
|
|
|
|
// so a two-word tooltip is not a wide box with two words in it.
|
|
|
|
|
const float text_w = ImGui::CalcTextSize(text.c_str(), nullptr, false, wrap).x;
|
|
|
|
|
const float width = (media.empty() ? std::min(wrap, std::ceil(text_w)) : wrap) + 2.f * style.WindowPadding.x;
|
|
|
|
|
|
|
|
|
|
// Beside the anchor (the panel, or the texture picker when the tooltip comes from inside it), never
|
|
|
|
|
// over it - the same reasoning, and the same placement, as the picker popup itself. ImGui normally
|
|
|
|
|
// keeps a tooltip glued to the mouse; it honours a position set here instead (see the Orca branch in
|
|
|
|
|
// imgui's Begin()), and still clamps the result inside the canvas.
|
|
|
|
|
const bool left = m_tooltip_anchor_min.x - gap - width >= 0.f;
|
|
|
|
|
const float x = left ? m_tooltip_anchor_min.x - gap - width : m_tooltip_anchor_max.x + gap;
|
|
|
|
|
ImGui::SetNextWindowPos(ImVec2(x, ImGui::GetItemRectMin().y)); // level with the control it explains
|
|
|
|
|
ImGui::SetNextWindowSize(ImVec2(width, 0.f)); // height still follows the content
|
|
|
|
|
|
|
|
|
|
ImGui::BeginTooltip();
|
|
|
|
|
ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));
|
|
|
|
|
ImGui::PushTextWrapPos(0.f); // the window's own edge, which is what the fixed width above sets
|
|
|
|
|
ImGui::TextUnformatted(text.c_str());
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
ImGui::PopStyleColor();
|
|
|
|
|
render_tip_media(media, width - 2.f * style.WindowPadding.x);
|
|
|
|
|
ImGui::EndTooltip();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GLGizmoTextureDisplacement::render_tip_media(const std::vector<TipMedia> &media, float width)
|
|
|
|
|
{
|
|
|
|
|
// Collect what is actually there first: the layout divides the width between the illustrations, so
|
|
|
|
|
// one that is missing has to drop out of the count rather than leave a gap.
|
|
|
|
|
std::vector<std::pair<const AnimatedTexture *, const TipMedia *>> shown;
|
|
|
|
|
std::string key;
|
|
|
|
|
for (const TipMedia &m : media) {
|
|
|
|
|
const AnimatedTexture *texture =
|
|
|
|
|
m_tip_media.get((boost::filesystem::path(resources_dir()) / "images" / "texture_displacement" / m.file).string());
|
|
|
|
|
if (texture != nullptr && texture->is_valid() && texture->width() > 0 && texture->height() > 0) {
|
|
|
|
|
shown.emplace_back(texture, &m);
|
|
|
|
|
key += m.file + "|";
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (shown.empty())
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
// Each animation plays from its first frame when its tooltip opens: these are short loops showing
|
|
|
|
|
// one setting going from one value to another, and joining one halfway through shows the user the
|
|
|
|
|
// end of a change they never saw the start of.
|
|
|
|
|
// Restart on a different tooltip, and on the same one opening again - which is what a gap in the
|
|
|
|
|
// frames it was drawn on means, since it is drawn on every frame for as long as it is on screen.
|
|
|
|
|
const int this_frame = ImGui::GetFrameCount();
|
|
|
|
|
if (key != m_tip_media_key || this_frame != m_tip_media_frame + 1) {
|
|
|
|
|
m_tip_media_key = key;
|
|
|
|
|
m_tip_media_start = tip_now();
|
|
|
|
|
}
|
|
|
|
|
m_tip_media_frame = this_frame;
|
|
|
|
|
const double time = tip_now() - m_tip_media_start;
|
|
|
|
|
|
|
|
|
|
const ImGuiStyle &style = ImGui::GetStyle();
|
|
|
|
|
const float spacing = style.ItemSpacing.x;
|
|
|
|
|
const float cell_w = (width - spacing * float(shown.size() - 1)) / float(shown.size());
|
|
|
|
|
const ImU32 frame = ImGui::GetColorU32(ImGuiCol_Text, 0.25f);
|
|
|
|
|
|
|
|
|
|
// A single illustration would otherwise be drawn at the tooltip's full width, and a square one at
|
|
|
|
|
// full width is a picture as tall as the text beside it is long. Capped and centred in its column.
|
|
|
|
|
const float max_h = m_imgui->scaled(10.f);
|
|
|
|
|
|
|
|
|
|
ImGui::Spacing();
|
|
|
|
|
for (size_t i = 0; i < shown.size(); ++i) {
|
|
|
|
|
const AnimatedTexture *texture = shown[i].first;
|
|
|
|
|
const TipMedia &entry = *shown[i].second;
|
|
|
|
|
const float cell_x = ImGui::GetCursorPosX();
|
|
|
|
|
|
|
|
|
|
float w = cell_w;
|
|
|
|
|
float h = std::round(cell_w * float(texture->height()) / float(texture->width()));
|
|
|
|
|
if (h > max_h) {
|
|
|
|
|
w = std::round(max_h * float(texture->width()) / float(texture->height()));
|
|
|
|
|
h = max_h;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImGui::BeginGroup();
|
|
|
|
|
const unsigned int id = texture->texture_id(time);
|
|
|
|
|
ImGui::SetCursorPosX(cell_x + std::round(0.5f * (cell_w - w)));
|
|
|
|
|
ImGui::Image((ImTextureID) (intptr_t) id, ImVec2(w, h));
|
|
|
|
|
ImGui::GetWindowDrawList()->AddRect(ImGui::GetItemRectMin(), ImGui::GetItemRectMax(), frame, style.FrameRounding);
|
|
|
|
|
// The canvas only redraws on demand, so an animation has to ask for its own frames. It asks for
|
|
|
|
|
// them as fast as they come rather than pacing itself with a timer: which frame is drawn is
|
|
|
|
|
// decided by the clock above, so a redraw that arrives early simply draws the same frame again,
|
|
|
|
|
// and a guaranteed frame is worth more here than a frugal one that may never arrive.
|
|
|
|
|
if (texture->is_animated())
|
|
|
|
|
m_parent.request_extra_frame();
|
|
|
|
|
|
|
|
|
|
if (!entry.caption.empty()) {
|
|
|
|
|
// Centred under its own illustration, and never wider than its column.
|
|
|
|
|
const std::string caption = into_u8(entry.caption);
|
|
|
|
|
const float text_w = ImGui::CalcTextSize(caption.c_str()).x;
|
|
|
|
|
ImGui::SetCursorPosX(cell_x + std::max(0.f, (cell_w - text_w) * 0.5f));
|
|
|
|
|
ImGui::PushTextWrapPos(cell_x + cell_w);
|
|
|
|
|
ImGui::TextDisabled("%s", caption.c_str());
|
|
|
|
|
ImGui::PopTextWrapPos();
|
|
|
|
|
}
|
|
|
|
|
ImGui::EndGroup();
|
|
|
|
|
|
|
|
|
|
if (i + 1 < shown.size())
|
|
|
|
|
ImGui::SameLine(0.f, spacing);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GLGizmoTextureDisplacement::add_texture_layer()
|
|
|
|
|
{
|
|
|
|
|
ModelVolume *mv = texture_volume();
|
|
|
|
@@ -3979,6 +4162,14 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// While this popup is up its own tooltips belong beside it, not beside the panel it is already
|
|
|
|
|
// covering the left of. Restored before every exit below.
|
|
|
|
|
const ImVec2 prev_anchor_min = m_tooltip_anchor_min;
|
|
|
|
|
const ImVec2 prev_anchor_max = m_tooltip_anchor_max;
|
|
|
|
|
m_tooltip_anchor_min = ImGui::GetWindowPos();
|
|
|
|
|
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
|
|
|
|
|
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
|
|
|
|
|
|
|
|
|
|
m_imgui->push_bold_font();
|
|
|
|
|
m_imgui->text(_L("Choose a texture"));
|
|
|
|
|
m_imgui->pop_bold_font();
|
|
|
|
@@ -4037,7 +4228,7 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|
|
|
|
if (entry.path == layer->path)
|
|
|
|
|
dl->AddRect(ImVec2(a.x - 1.f, a.y - 1.f), ImVec2(b.x + 1.f, b.y + 1.f), selected_col, 0.f, 0, 2.f);
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(entry.name, m_imgui->scaled(18.f));
|
|
|
|
|
panel_tooltip(entry.name);
|
|
|
|
|
|
|
|
|
|
if (user && ImGui::IsMouseHoveringRect(a, b)) {
|
|
|
|
|
const float r = std::round(tile * 0.17f);
|
|
|
|
@@ -4050,7 +4241,7 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|
|
|
|
dl->AddLine(ImVec2(c.x - k, c.y - k), ImVec2(c.x + k, c.y + k), IM_COL32_WHITE, 1.5f);
|
|
|
|
|
dl->AddLine(ImVec2(c.x - k, c.y + k), ImVec2(c.x + k, c.y - k), IM_COL32_WHITE, 1.5f);
|
|
|
|
|
if (hot)
|
|
|
|
|
m_imgui->tooltip(_u8L("Remove from My textures"), m_imgui->scaled(18.f));
|
|
|
|
|
panel_tooltip(_u8L("Remove from My textures"));
|
|
|
|
|
if (remove_clicked)
|
|
|
|
|
to_remove = entry;
|
|
|
|
|
}
|
|
|
|
@@ -4077,10 +4268,9 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|
|
|
|
if (ImGui::Button("+##import_texture", ImVec2(tile, tile)))
|
|
|
|
|
import_clicked = true;
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
|
|
|
|
|
"bakes from and saved to your personal texture folder, kept separate from the "
|
|
|
|
|
"textures shipped with OrcaSlicer."),
|
|
|
|
|
m_imgui->scaled(20.f));
|
|
|
|
|
panel_tooltip(_u8L("Import your own image as a height map. It is converted to the format the slicer "
|
|
|
|
|
"bakes from and saved to your personal texture folder, kept separate from the "
|
|
|
|
|
"textures shipped with OrcaSlicer."));
|
|
|
|
|
end_grid(origin, count + 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -4090,6 +4280,8 @@ void GLGizmoTextureDisplacement::render_texture_library_popup(const ImVec2 &pane
|
|
|
|
|
|
|
|
|
|
if (chosen || import_clicked)
|
|
|
|
|
ImGui::CloseCurrentPopup();
|
|
|
|
|
m_tooltip_anchor_min = prev_anchor_min;
|
|
|
|
|
m_tooltip_anchor_max = prev_anchor_max;
|
|
|
|
|
ImGui::EndPopup();
|
|
|
|
|
|
|
|
|
|
// Only once the loop over the library is done: importing, picking (which reorders the recently used)
|
|
|
|
@@ -6066,18 +6258,16 @@ void GLGizmoTextureDisplacement::render_debug_stage_panel(ModelVolume *mv)
|
|
|
|
|
run_stage_debug();
|
|
|
|
|
m_imgui->disabled_end();
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(_u8L("Run the same bake the Bake button runs, keeping the mesh after every "
|
|
|
|
|
"stage, and step through them here. Nothing is committed to the model - "
|
|
|
|
|
"this is for finding which stage a bad result came from."),
|
|
|
|
|
m_imgui->scaled(20.f));
|
|
|
|
|
panel_tooltip(_u8L("Run the same bake the Bake button runs, keeping the mesh after every "
|
|
|
|
|
"stage, and step through them here. Nothing is committed to the model - "
|
|
|
|
|
"this is for finding which stage a bad result came from."));
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
m_imgui->bbl_checkbox(_L("Check topology"), m_debug_check_topology);
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(_u8L("Count open and non-manifold edges after each stage, which is how a stage "
|
|
|
|
|
"that tore the mesh is spotted. It scans every edge, so it adds noticeably "
|
|
|
|
|
"to the capture on a fine bake."),
|
|
|
|
|
m_imgui->scaled(20.f));
|
|
|
|
|
panel_tooltip(_u8L("Count open and non-manifold edges after each stage, which is how a stage "
|
|
|
|
|
"that tore the mesh is spotted. It scans every edge, so it adds noticeably "
|
|
|
|
|
"to the capture on a fine bake."));
|
|
|
|
|
|
|
|
|
|
if (m_debug_stages.empty())
|
|
|
|
|
return;
|
|
|
|
@@ -6145,9 +6335,8 @@ void GLGizmoTextureDisplacement::render_debug_stage_panel(ModelVolume *mv)
|
|
|
|
|
show_error(nullptr, _u8L("Could not write the stage meshes."));
|
|
|
|
|
}
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(_u8L("Write every captured stage out as an OBJ file, so they can be opened "
|
|
|
|
|
"side by side in a mesh viewer."),
|
|
|
|
|
m_imgui->scaled(20.f));
|
|
|
|
|
panel_tooltip(_u8L("Write every captured stage out as an OBJ file, so they can be opened "
|
|
|
|
|
"side by side in a mesh viewer."));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareParams ¶ms,
|
|
|
|
@@ -6274,6 +6463,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
|
|
|
|
|
ImGuiWrapper::push_toolbar_style(m_parent.get_scale());
|
|
|
|
|
GizmoImguiBegin(get_name(), flags);
|
|
|
|
|
// Where this frame's tooltips place themselves (see panel_tooltip()). The size is last frame's -
|
|
|
|
|
// this window auto-resizes, so its height is only known once its content has been laid out - which
|
|
|
|
|
// is close enough for a tooltip that needs a frame of hovering to appear in the first place.
|
|
|
|
|
m_tooltip_anchor_min = ImGui::GetWindowPos();
|
|
|
|
|
m_tooltip_anchor_max = ImVec2(m_tooltip_anchor_min.x + ImGui::GetWindowSize().x,
|
|
|
|
|
m_tooltip_anchor_min.y + ImGui::GetWindowSize().y);
|
|
|
|
|
ensure_panel_icons();
|
|
|
|
|
process_uv_commands(); // clicks from the UV editor pane, run here where the GL context is current
|
|
|
|
|
// After the pane's commands, which can close it or change the view, and before the View row, so the row
|
|
|
|
@@ -6300,7 +6495,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
const float gap_s = std::round(m_imgui->scaled(0.4f));
|
|
|
|
|
const float label_w = m_imgui->scaled(4.8f); // label column of a label + slider row
|
|
|
|
|
const float card_pad = std::round(m_imgui->scaled(0.55f));
|
|
|
|
|
const float wrap_w = m_imgui->scaled(20.f);
|
|
|
|
|
const ImVec4 orca = ImGuiWrapper::COL_ORCA;
|
|
|
|
|
const ImVec4 col_link = ImGuiWrapper::COL_ORCA;
|
|
|
|
|
const ImVec4 col_frame = ImGui::GetStyleColorVec4(ImGuiCol_Separator);
|
|
|
|
@@ -6314,9 +6508,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
ImGuiWrapper::pop_combo_style();
|
|
|
|
|
return changed;
|
|
|
|
|
};
|
|
|
|
|
const auto hover_tip = [&](const auto &text) {
|
|
|
|
|
const auto hover_tip = [&](const auto &text, const std::vector<TipMedia> &media = {}) {
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(text, wrap_w);
|
|
|
|
|
panel_tooltip(text, media);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Framed icon toggle: a 1 px frame, and when on a teal frame over a teal tint with the icon in its
|
|
|
|
@@ -6324,7 +6518,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
// `unavailable`, when not empty, draws it faded but still hoverable, so the tooltip can say why it
|
|
|
|
|
// cannot be used - a disabled ImGui item would swallow the hover and leave the user guessing.
|
|
|
|
|
const auto icon_toggle = [&](int uid, const std::string &iconfile, bool active, float sz, const wxString &label,
|
|
|
|
|
const wxString &tip_text, const wxString &unavailable = wxString()) -> bool {
|
|
|
|
|
const wxString &tip_text, const std::vector<TipMedia> &media = {},
|
|
|
|
|
const wxString &unavailable = wxString()) -> bool {
|
|
|
|
|
const bool na = !unavailable.empty();
|
|
|
|
|
const auto it = m_panel_icon_map.find(iconfile);
|
|
|
|
|
const float pad = std::max(1.f, std::round(sz * 0.14f));
|
|
|
|
@@ -6351,12 +6546,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
ImGui::PopStyleVar(2);
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
if (ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(na ? tip_text + "\n\n" + unavailable : tip_text, wrap_w);
|
|
|
|
|
panel_tooltip(na ? tip_text + "\n\n" + unavailable : tip_text, media);
|
|
|
|
|
return clicked && !na;
|
|
|
|
|
};
|
|
|
|
|
// Borderless icon button (non-toggle): always the grey monochrome variant.
|
|
|
|
|
const auto icon_button = [&](int uid, const std::string &iconfile, float sz, const wxString &label,
|
|
|
|
|
const wxString &tip_text) -> bool {
|
|
|
|
|
const wxString &tip_text, const std::vector<TipMedia> &media = {}) -> bool {
|
|
|
|
|
const auto it = m_panel_icon_map.find(iconfile);
|
|
|
|
|
const float pad = std::max(1.f, std::round(sz * 0.14f));
|
|
|
|
|
bool clicked = false;
|
|
|
|
@@ -6374,7 +6569,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
ImGui::PopStyleVar();
|
|
|
|
|
ImGui::PopID();
|
|
|
|
|
if (!tip_text.empty() && ImGui::IsItemHovered())
|
|
|
|
|
m_imgui->tooltip(tip_text, wrap_w);
|
|
|
|
|
panel_tooltip(tip_text, media);
|
|
|
|
|
return clicked;
|
|
|
|
|
};
|
|
|
|
|
// A short vertical rule between groups of buttons on one row; call it right after an item.
|
|
|
|
@@ -6453,10 +6648,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Draws `slider` at `item_w` (SetNextItemWidth's convention: negative measures back from the right
|
|
|
|
|
// edge, and the caller has already left edit_icon_room in it), then the edit icon. `tip` and the
|
|
|
|
|
// range go on both. Returns whatever the slider returned.
|
|
|
|
|
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::string &range,
|
|
|
|
|
const std::function<bool()> &slider) -> bool {
|
|
|
|
|
// edge, and the caller has already left edit_icon_room in it), then the edit icon. The range goes on
|
|
|
|
|
// both tooltips; `tip` and its illustrations `media` on the slider's. Returns whatever the slider returned.
|
|
|
|
|
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::vector<TipMedia> &media,
|
|
|
|
|
const std::string &range, const std::function<bool()> &slider) -> bool {
|
|
|
|
|
// Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next*
|
|
|
|
|
// frame, so it has to be issued before the slider it names, not from the icon beside it.
|
|
|
|
|
if (m_slider_type_request == id) {
|
|
|
|
@@ -6472,7 +6667,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
wxString full = tip;
|
|
|
|
|
if (!full.IsEmpty())
|
|
|
|
|
full += "\n\n";
|
|
|
|
|
m_imgui->tooltip(full + from_u8(range), wrap_w);
|
|
|
|
|
panel_tooltip(full + from_u8(range), media);
|
|
|
|
|
}
|
|
|
|
|
// SameLine() puts the cursor back at the slider's own top (ItemSize records the Y the item was
|
|
|
|
|
// drawn at), so an icon of the same height lands on the track wherever the row placed it.
|
|
|
|
@@ -6489,15 +6684,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
// already given, so no row states its bounds or its format twice and the tooltip cannot drift from
|
|
|
|
|
// the track. `item_w` is as typed_row() takes it.
|
|
|
|
|
const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max, const char *format,
|
|
|
|
|
bool log, const wxString &tip) -> bool {
|
|
|
|
|
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
|
|
|
|
bool log, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
|
|
|
|
return typed_row(id, item_w, tip, media, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
|
|
|
|
return ImGui::SliderFloat(id, v, v_min, v_max, format,
|
|
|
|
|
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max, const char *format,
|
|
|
|
|
bool log, const wxString &tip) -> bool {
|
|
|
|
|
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
|
|
|
|
bool log, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
|
|
|
|
return typed_row(id, item_w, tip, media, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
|
|
|
|
|
return ImGui::SliderInt(id, v, v_min, v_max, format,
|
|
|
|
|
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
|
|
|
|
|
});
|
|
|
|
@@ -6505,14 +6700,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
|
|
|
|
|
// Label column + the slider filling the rest of the row.
|
|
|
|
|
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
|
|
|
|
|
bool log, float right_inset, const wxString &tip) -> bool {
|
|
|
|
|
bool log, float right_inset, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
|
|
|
|
slider_label(label);
|
|
|
|
|
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip);
|
|
|
|
|
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip, media);
|
|
|
|
|
};
|
|
|
|
|
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
|
|
|
|
|
float right_inset, const wxString &tip) -> bool {
|
|
|
|
|
float right_inset, const wxString &tip, const std::vector<TipMedia> &media = {}) -> bool {
|
|
|
|
|
slider_label(label);
|
|
|
|
|
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip);
|
|
|
|
|
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip, media);
|
|
|
|
|
};
|
|
|
|
|
const auto layer_name = [](const TextureDisplacementLayer &l) {
|
|
|
|
|
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
|
|
|
|
@@ -6668,11 +6863,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
ImGui::SetCursorPosX(toggles_x);
|
|
|
|
|
if (icon_toggle(820, "texture_displacement_add.svg", !m_erase_mode, icon_sm, _L("Paint"),
|
|
|
|
|
_L("Paint - add the active layer where you click or drag. The right button erases, and so does "
|
|
|
|
|
"holding Shift.")))
|
|
|
|
|
"holding Shift."),
|
|
|
|
|
{ { "paint.gif" } }))
|
|
|
|
|
m_erase_mode = false;
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(821, "texture_displacement_add_negative.svg", m_erase_mode, icon_sm, _L("Erase"),
|
|
|
|
|
_L("Erase - remove the active layer where you click or drag. The right button paints.")))
|
|
|
|
|
_L("Erase - remove the active layer where you click or drag. The right button paints."),
|
|
|
|
|
{ { "erase.gif" } }))
|
|
|
|
|
m_erase_mode = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -6686,20 +6883,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
const bool is_area_mode = m_tool_type == ToolType::SMART_FILL;
|
|
|
|
|
const float row_y = ImGui::GetCursorPosY();
|
|
|
|
|
if (icon_toggle(801, "texture_displacement_brush.svg", is_brush_mode, icon_md, _L("Brush"),
|
|
|
|
|
_L("Brush - paint over the surface by dragging"))) {
|
|
|
|
|
_L("Brush - paint over the surface by dragging"), { { "paint.gif" } })) {
|
|
|
|
|
m_tool_type = ToolType::BRUSH;
|
|
|
|
|
if (m_cursor_type == TriangleSelector::CursorType::POINTER)
|
|
|
|
|
m_cursor_type = TriangleSelector::CursorType::CIRCLE;
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(802, "texture_displacement_face.svg", is_face_mode, icon_md, _L("Face"), _L("Face - click individual triangles"))) {
|
|
|
|
|
if (icon_toggle(802, "texture_displacement_face.svg", is_face_mode, icon_md, _L("Face"), _L("Face - click individual triangles"),
|
|
|
|
|
{ { "tool_face.gif" } })) {
|
|
|
|
|
m_tool_type = ToolType::BRUSH;
|
|
|
|
|
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(803, "texture_displacement_connected_area.svg", is_area_mode, icon_md, _L("Connected area"),
|
|
|
|
|
_L("Connected area - flood-fill the region reachable without crossing an edge sharper than the "
|
|
|
|
|
"angle threshold"))) {
|
|
|
|
|
"angle threshold"),
|
|
|
|
|
{ { "tool_smart_fill.gif" } })) {
|
|
|
|
|
m_tool_type = ToolType::SMART_FILL;
|
|
|
|
|
m_cursor_type = TriangleSelector::CursorType::POINTER;
|
|
|
|
|
}
|
|
|
|
@@ -6713,11 +6912,13 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
wxString();
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(806, "texture_displacement_select_all.svg", false, icon_md, _L("Select whole model"),
|
|
|
|
|
_L("Select whole model - paint every face of the model with the active layer"), whole_na))
|
|
|
|
|
_L("Select whole model - paint every face of the model with the active layer"),
|
|
|
|
|
{ { "select_all.png" } }, whole_na))
|
|
|
|
|
select_whole_model();
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(807, "texture_displacement_erase_all.svg", false, icon_md, _L("Erase whole model"),
|
|
|
|
|
_L("Erase whole model - clear the active layer's paint from every face"), erase_na)) {
|
|
|
|
|
_L("Erase whole model - clear the active layer's paint from every face"),
|
|
|
|
|
{ { "erase_all.png" } }, erase_na)) {
|
|
|
|
|
Plater::TakeSnapshot snapshot(wxGetApp().plater(), _u8L("Reset texture displacement selection"),
|
|
|
|
|
UndoRedo::SnapshotType::GizmoAction);
|
|
|
|
|
int idx = -1;
|
|
|
|
@@ -6739,7 +6940,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
centre_on_row(frame_h);
|
|
|
|
|
typed_slider("##cursor_radius",
|
|
|
|
|
std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room),
|
|
|
|
|
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"));
|
|
|
|
|
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"),
|
|
|
|
|
{ { "cursor_size.gif" } });
|
|
|
|
|
vsep(icon_sm);
|
|
|
|
|
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
|
|
|
|
|
centre_on_row(icon_sm);
|
|
|
|
@@ -6755,7 +6957,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
centre_on_row(frame_h);
|
|
|
|
|
typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room),
|
|
|
|
|
&m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false,
|
|
|
|
|
_L("Angle threshold - the fill stops at edges sharper than this"));
|
|
|
|
|
_L("Angle threshold - the fill stops at edges sharper than this"),
|
|
|
|
|
{ { "smart_fill_angle_low.png", _L("Low") }, { "smart_fill_angle_high.png", _L("High") } });
|
|
|
|
|
} else {
|
|
|
|
|
centre_on_row(frame_h);
|
|
|
|
|
ImGui::AlignTextToFramePadding();
|
|
|
|
@@ -6791,19 +6994,21 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + m_imgui->scaled(2.6f)));
|
|
|
|
|
if (icon_toggle(701, "texture_displacement_real_preview.svg", cur_mode == 0, icon_md, _L("Normal"),
|
|
|
|
|
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update")))
|
|
|
|
|
_L("Normal - the real geometry, exactly what Bake will produce. Slower to update"),
|
|
|
|
|
{ { "tile_on.png" } }))
|
|
|
|
|
new_mode = picked = 0;
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(702, "texture_displacement_fast_preview.svg", cur_mode == 1, icon_md, _L("Fast"),
|
|
|
|
|
_L("Fast - the relief is only shaded on, not built, and only for the layer you are "
|
|
|
|
|
"editing. Updates instantly while you paint")))
|
|
|
|
|
"editing. Updates instantly while you paint"),
|
|
|
|
|
{ { "view_fast.png" } }))
|
|
|
|
|
new_mode = picked = 1;
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(703, "texture_displacement_checker.svg", cur_mode == 2, icon_md, _L("Checker"),
|
|
|
|
|
_L("Checker - a test grid instead of the texture. Where the squares stay square the "
|
|
|
|
|
"texture is undistorted; where they stretch, it will too. Opens the UV editor if "
|
|
|
|
|
"it is closed"),
|
|
|
|
|
uv_view_na)) {
|
|
|
|
|
{ { "view_checker.png" } }, uv_view_na)) {
|
|
|
|
|
new_mode = picked = 2;
|
|
|
|
|
open_uv_editor = true;
|
|
|
|
|
}
|
|
|
|
@@ -6811,13 +7016,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
if (icon_toggle(704, "texture_displacement_distortion.svg", cur_mode == 3, icon_md, _L("Distortion"),
|
|
|
|
|
_L("Distortion - blue-to-red stretch heatmap over the unwrap. Opens the UV editor if "
|
|
|
|
|
"it is closed"),
|
|
|
|
|
uv_view_na)) {
|
|
|
|
|
{ { "view_distortion.png" } }, uv_view_na)) {
|
|
|
|
|
new_mode = picked = 3;
|
|
|
|
|
open_uv_editor = true;
|
|
|
|
|
}
|
|
|
|
|
vsep(icon_md);
|
|
|
|
|
if (icon_toggle(705, "texture_displacement_wireframe.svg", m_wireframe_overlay, icon_md, _L("Wireframe"),
|
|
|
|
|
_L("Wireframe - overlay the mesh edges; independent of the view above")))
|
|
|
|
|
_L("Wireframe - overlay the mesh edges; independent of the view above"),
|
|
|
|
|
{ { "view_wireframe.png" } }))
|
|
|
|
|
wf_toggle = true;
|
|
|
|
|
|
|
|
|
|
const std::string auto_label = _u8L("Auto");
|
|
|
|
@@ -7038,7 +7244,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
// 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,
|
|
|
|
|
_L("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."));
|
|
|
|
|
"millimetres. Black does not move it at all, unless Midlevel says otherwise."),
|
|
|
|
|
{ { "depth.gif" } });
|
|
|
|
|
// Mapped by the model's own UV layout there is no millimetre scale to set: the layout is the
|
|
|
|
|
// placement (see TextureProjectionMethod::ModelUV), so the slider would move nothing. The row
|
|
|
|
|
// states why rather than hiding: float_row()'s tooltip is shown for a disabled slider too.
|
|
|
|
@@ -7050,11 +7257,15 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
"copy of the texture covers it exactly, so there is no tile size to set.") :
|
|
|
|
|
_L("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 "
|
|
|
|
|
"single copy."));
|
|
|
|
|
"single copy."),
|
|
|
|
|
tile_size_na ? std::vector<TipMedia>{} :
|
|
|
|
|
std::vector<TipMedia>{ { "tile_size_large.png", _L("Larger") },
|
|
|
|
|
{ "tile_size_small.png", _L("Smaller") } });
|
|
|
|
|
m_imgui->disabled_end();
|
|
|
|
|
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
|
|
|
|
|
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
|
|
|
|
|
"edge of the model."));
|
|
|
|
|
"edge of the model."),
|
|
|
|
|
{ { "rotation.gif" } });
|
|
|
|
|
|
|
|
|
|
if (m_layer_expanded[size_t(layer.slot)]) {
|
|
|
|
|
{
|
|
|
|
@@ -7074,7 +7285,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
"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."));
|
|
|
|
|
"cut can pass through the other side."),
|
|
|
|
|
{ { "midlevel.gif" } });
|
|
|
|
|
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."));
|
|
|
|
@@ -7084,12 +7296,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad,
|
|
|
|
|
_L("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."));
|
|
|
|
|
"costs fine detail."),
|
|
|
|
|
{ { "smoothing_off.png", _L("Off") }, { "smoothing_on.png", _L("On") } });
|
|
|
|
|
|
|
|
|
|
// 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."));
|
|
|
|
|
"blends into the bare surface instead of stopping at a step."),
|
|
|
|
|
{ { "edge_fade_off.png", _L("Off") }, { "edge_fade_on.png", _L("On") } });
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
m_imgui->disabled_begin(!layer.edge_smoothing);
|
|
|
|
|
m_preview_params_dirty |= typed_slider("##edge_amount", -(card_pad + edit_icon_room),
|
|
|
|
@@ -7103,7 +7317,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
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."));
|
|
|
|
|
"round. The same as using a negative of the image."),
|
|
|
|
|
{ { "invert_off.png", _L("Off") }, { "invert_on.png", _L("On") } });
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + 0.5f * (ImGui::GetWindowContentRegionMax().x - card_pad - x0)));
|
|
|
|
|
|
|
|
|
@@ -7119,13 +7334,12 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
}
|
|
|
|
|
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);
|
|
|
|
|
panel_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."));
|
|
|
|
|
|
|
|
|
|
// The rest of colour belongs to the whole stack, not to this layer, so it only appears once -
|
|
|
|
|
// under whichever layer turned colour on.
|
|
|
|
@@ -7135,7 +7349,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
m_preview_params_dirty = true;
|
|
|
|
|
hover_tip(_u8L("Interleaves two filaments to fake the colors in between, so a handful "
|
|
|
|
|
"of filaments can cover a photo or a gradient. An image of flat colors "
|
|
|
|
|
"prints the same either way. Off uses one filament per area."));
|
|
|
|
|
"prints the same either way. Off uses one filament per area."),
|
|
|
|
|
{ { "mix_off.png", _L("Off") }, { "mix_on.png", _L("On") } });
|
|
|
|
|
if (opts.color_mix_enabled) {
|
|
|
|
|
cached_palette(); // brings m_palette_filaments up to date
|
|
|
|
|
// Every mix can become a filament slot, and there are only so many beside the
|
|
|
|
@@ -7143,7 +7358,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
|
|
|
|
|
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad,
|
|
|
|
|
_L("The most mixed filaments a bake may add. They are picked from the "
|
|
|
|
|
"texture's colors, and only the ones the bake actually uses are created.")))
|
|
|
|
|
"texture's colors, and only the ones the bake actually uses are created."),
|
|
|
|
|
{ { "mixed_colors_few.png", _L("Few") }, { "mixed_colors_many.png", _L("Many") } }))
|
|
|
|
|
m_preview_params_dirty = true;
|
|
|
|
|
// After the slider, so a change shows in the same frame.
|
|
|
|
|
const int n_colors = int(cached_palette().size());
|
|
|
|
@@ -7157,7 +7373,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad,
|
|
|
|
|
_L("Cleans up single stray triangles of the wrong color, which detail finer "
|
|
|
|
|
"than the mesh leaves behind. Raise it if the result looks speckled, "
|
|
|
|
|
"lower it if small features are being swallowed.")))
|
|
|
|
|
"lower it if small features are being swallowed."),
|
|
|
|
|
{ { "denoise_off.png", _L("Off") }, { "denoise_on.png", _L("On") } }))
|
|
|
|
|
m_preview_params_dirty = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
@@ -7174,26 +7391,33 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
const char *file;
|
|
|
|
|
wxString label;
|
|
|
|
|
wxString tip;
|
|
|
|
|
const char *picture; // the same ball with the checker laid on it this way, or null for none
|
|
|
|
|
};
|
|
|
|
|
const MappingIcon mappings[] = {
|
|
|
|
|
{ "menu_obj_cube.svg", _L("Triplanar (blended)"),
|
|
|
|
|
_L("Triplanar - projects the texture from all three axes at once and blends between them, so a "
|
|
|
|
|
"patch wrapping around a sharp edge has no seam.") },
|
|
|
|
|
"patch wrapping around a sharp edge has no seam."),
|
|
|
|
|
"map_cube.png" },
|
|
|
|
|
{ "menu_obj_cylinder.svg", _L("Cylindrical"),
|
|
|
|
|
_L("Cylindrical - wraps the texture around the painted area's own center, for round shapes.") },
|
|
|
|
|
_L("Cylindrical - wraps the texture around the painted area's own center, for round shapes."),
|
|
|
|
|
"map_cylinder.png" },
|
|
|
|
|
{ "menu_obj_sphere.svg", _L("Spherical"),
|
|
|
|
|
_L("Spherical - wraps the texture around the painted area's own center in both directions.") },
|
|
|
|
|
_L("Spherical - wraps the texture around the painted area's own center in both directions."),
|
|
|
|
|
"map_sphere.png" },
|
|
|
|
|
{ "texture_displacement_map_unwrap.svg", _L("Unwrap (LSCM)"),
|
|
|
|
|
_L("Unwrap - flattens the painted area and maps the texture onto it with as little stretching as "
|
|
|
|
|
"possible. The area is cut into pieces at its sharp edges first (see Seam angle), so each "
|
|
|
|
|
"piece can lie flat on its own.") },
|
|
|
|
|
"piece can lie flat on its own."),
|
|
|
|
|
"map_unwrap.png" },
|
|
|
|
|
{ "texture_displacement_map_view.svg", _L("From view"),
|
|
|
|
|
_L("From view - projects straight onto the painted area from where you are looking, like a slide "
|
|
|
|
|
"projector.") },
|
|
|
|
|
"projector."),
|
|
|
|
|
"map_view.png" },
|
|
|
|
|
{ "texture_displacement_map_model_uv.svg", _L("Model UV"),
|
|
|
|
|
_L("Model UV - uses the texture coordinates the model itself came with, so a texture made for "
|
|
|
|
|
"that model lands exactly where it was painted. Nothing is unwrapped, and Tile size does "
|
|
|
|
|
"not apply: the model's own layout is the placement.") },
|
|
|
|
|
"not apply: the model's own layout is the placement."),
|
|
|
|
|
nullptr },
|
|
|
|
|
};
|
|
|
|
|
// Only a volume that actually carries a UV channel can be mapped by it. Shown
|
|
|
|
|
// greyed out rather than hidden, so the reason is visible instead of the control
|
|
|
|
@@ -7208,8 +7432,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
const auto method = static_cast<TextureProjectionMethod>(mi);
|
|
|
|
|
const wxString na = (method == TextureProjectionMethod::ModelUV) ? model_uv_na : wxString();
|
|
|
|
|
const std::vector<TipMedia> picture = mappings[mi].picture != nullptr ?
|
|
|
|
|
std::vector<TipMedia>{ { mappings[mi].picture } } :
|
|
|
|
|
std::vector<TipMedia>{};
|
|
|
|
|
if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label,
|
|
|
|
|
mappings[mi].tip, na) &&
|
|
|
|
|
mappings[mi].tip, picture, na) &&
|
|
|
|
|
layer.projection_method != method) {
|
|
|
|
|
// Capture the current view the moment "From view" is chosen, so it does something
|
|
|
|
|
// sensible immediately rather than projecting from a stale/default direction.
|
|
|
|
@@ -7314,19 +7541,20 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
const float x0 = ImGui::GetCursorPosX();
|
|
|
|
|
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Tile") + "##tile_enabled", layer.tile_enabled);
|
|
|
|
|
hover_tip(_u8L("Repeats the texture across the painted area. Off places one copy, like a decal, "
|
|
|
|
|
"at the size set by Tile size."));
|
|
|
|
|
"at the size set by Tile size."),
|
|
|
|
|
{ { "tile_off.png", _L("Off") }, { "tile_on.png", _L("On") } });
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
|
|
|
|
|
const wxString tile_na = layer.tile_enabled ? wxString() : _L("Turn Tile on to choose how the texture repeats.");
|
|
|
|
|
if (icon_toggle(840, "texture_displacement_tile_repeat.svg", layer.tile_method == static_cast<TextureTileMethod>(0),
|
|
|
|
|
icon_sm, _L("Repeat"), _L("Repeat"), tile_na)) {
|
|
|
|
|
icon_sm, _L("Repeat"), _L("Repeat"), { { "tile_repeat.png" } }, tile_na)) {
|
|
|
|
|
layer.tile_method = static_cast<TextureTileMethod>(0);
|
|
|
|
|
m_preview_params_dirty = true;
|
|
|
|
|
}
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(841, "menu_mirror_x.svg", layer.tile_method == static_cast<TextureTileMethod>(1), icon_sm,
|
|
|
|
|
_L("Mirrored repeat"), _L("Mirrored repeat - every other tile is flipped, so edges meet seamlessly"),
|
|
|
|
|
tile_na)) {
|
|
|
|
|
{ { "tile_mirror.png" } }, tile_na)) {
|
|
|
|
|
layer.tile_method = static_cast<TextureTileMethod>(1);
|
|
|
|
|
m_preview_params_dirty = true;
|
|
|
|
|
}
|
|
|
|
@@ -7371,15 +7599,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
}
|
|
|
|
|
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. Override replaces them, "
|
|
|
|
|
"so only this layer's relief is left where it is painted. 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);
|
|
|
|
|
panel_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. Override replaces them, "
|
|
|
|
|
"so only this layer's relief is left where it is painted. 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."),
|
|
|
|
|
base_layer ? std::vector<TipMedia>{} :
|
|
|
|
|
std::vector<TipMedia>{ { "blend_add.png", from_u8(blend_add) },
|
|
|
|
|
{ "blend_multiply.png", from_u8(blend_multiply) } });
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine(0.f, gap_s);
|
|
|
|
|
if (icon_toggle(842, "texture_displacement_adjust.svg", m_adjust_texture_mode, icon_sm, _L("Adjust placement"),
|
|
|
|
@@ -7562,7 +7792,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
}
|
|
|
|
|
hover_tip(_u8L("Adds triangles only where you painted, instead of everywhere. On a large part with a small "
|
|
|
|
|
"decal that is the difference between thousands of triangles and millions, and your paint "
|
|
|
|
|
"survives the refinement."));
|
|
|
|
|
"survives the refinement."),
|
|
|
|
|
{ { "painted_only_off.png", _L("Off") }, { "painted_only_on.png", _L("On") } });
|
|
|
|
|
|
|
|
|
|
if (m_subdivide_adaptive) {
|
|
|
|
|
if (m_subdivide_target_mm <= 0.f && mv != nullptr) {
|
|
|
|
@@ -7590,7 +7821,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
preview_live();
|
|
|
|
|
hover_tip(_u8L("Spends the triangles where the texture actually bends - packed along ridges and edges, "
|
|
|
|
|
"sparse over flat ground - instead of spreading them evenly. The same detail for fewer "
|
|
|
|
|
"triangles on most textures."));
|
|
|
|
|
"triangles on most textures."),
|
|
|
|
|
{ { "adaptive_off.png", _L("Off") }, { "adaptive_on.png", _L("On") } });
|
|
|
|
|
|
|
|
|
|
// The edge-length target is a baseline in both modes. In feature mode it is what guarantees the
|
|
|
|
|
// curvature test can actually see the texture: left too coarse, a big triangle over a fine
|
|
|
|
@@ -7643,7 +7875,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f,
|
|
|
|
|
_L("Splits every triangle of the model into four, this many times over. Each step "
|
|
|
|
|
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
|
|
|
|
|
"only the painted area unless you need the whole model finer."))) {
|
|
|
|
|
"only the painted area unless you need the whole model finer."),
|
|
|
|
|
{ { "subdivide.gif" } })) {
|
|
|
|
|
if (m_subdivide_editing)
|
|
|
|
|
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
|
|
|
|
|
m_parent.set_as_dirty();
|
|
|
|
@@ -7711,12 +7944,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
}
|
|
|
|
|
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f,
|
|
|
|
|
_L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
|
|
|
|
|
"even mesh; this is what makes one out of an uneven import."));
|
|
|
|
|
"even mesh; this is what makes one out of an uneven import."),
|
|
|
|
|
{ { "remesh_before.png", _L("Before") }, { "remesh_after.png", _L("After") } });
|
|
|
|
|
|
|
|
|
|
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
|
|
|
|
|
hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it "
|
|
|
|
|
"the remesher slides vertices along the surface and rounds every crisp edge off - "
|
|
|
|
|
"a cube comes back with wobbly edges."));
|
|
|
|
|
"a cube comes back with wobbly edges."),
|
|
|
|
|
{ { "preserve_edges_off.png", _L("Off") }, { "preserve_edges_on.png", _L("On") } });
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
|
|
|
|
|
typed_slider("##remesh_sharp_angle", -(1.f + edit_icon_room), &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", false,
|
|
|
|
@@ -7731,7 +7966,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
hover_tip(_u8L("Rebuilds the whole model with triangles close to this edge length - splitting the big "
|
|
|
|
|
"ones and merging the small ones - so displacement has an even density to work with. "
|
|
|
|
|
"Replaces the geometry; your paint is carried onto the new triangles spatially, so it "
|
|
|
|
|
"survives (already-baked relief is kept too)."));
|
|
|
|
|
"survives (already-baked relief is kept too)."),
|
|
|
|
|
{ { "remesh_before.png", _L("Before") }, { "remesh_after.png", _L("After") } });
|
|
|
|
|
|
|
|
|
|
// Settings for the whole stack rather than one layer. Standard mode pins them instead of showing them.
|
|
|
|
|
if (mv != nullptr) {
|
|
|
|
@@ -7747,7 +7983,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Smooth result"), opts.smooth_enabled);
|
|
|
|
|
hover_tip(_u8L("Smooths the geometry after the texture has been applied, to take the hard steps out of a "
|
|
|
|
|
"low-resolution image. Only what the displacement moved is touched. The Smoothing slider "
|
|
|
|
|
"on a layer is a different thing: it blurs the image before it is used."));
|
|
|
|
|
"on a layer is a different thing: it blurs the image before it is used."),
|
|
|
|
|
{ { "geometry_smoothing.gif" } });
|
|
|
|
|
if (opts.smooth_enabled) {
|
|
|
|
|
float percent = opts.smooth_strength * 100.f;
|
|
|
|
|
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f,
|
|
|
|
@@ -7807,7 +8044,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
"Relief already baked into the rest of the model is kept on top of it, so a "
|
|
|
|
|
"second bake somewhere else gets the same budget as the first. Raise it if the "
|
|
|
|
|
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
|
|
|
|
|
"the refinement produced, however many that is."))) {
|
|
|
|
|
"the refinement produced, however many that is."),
|
|
|
|
|
{ { "subdivide.gif" } })) {
|
|
|
|
|
opts.v2_max_triangles_k = shown_k;
|
|
|
|
|
m_preview_params_dirty = true;
|
|
|
|
|
}
|
|
|
|
@@ -7826,7 +8064,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
hover_tip(_u8L("Collapse the thin triangles refinement inherits from the model's own "
|
|
|
|
|
"tessellation, before displacement samples them. A sliver's three corners "
|
|
|
|
|
"land on three unrelated parts of the texture, which is what makes the "
|
|
|
|
|
"relief look jagged."));
|
|
|
|
|
"relief look jagged."),
|
|
|
|
|
{ { "collapse_slivers_off.png", _L("Off") }, { "collapse_slivers_on.png", _L("On") } });
|
|
|
|
|
}
|
|
|
|
|
// Below both pipelines' own settings, because it replays whichever of them is selected.
|
|
|
|
|
if (SHOW_PIPELINE_DEV_CONTROLS)
|
|
|
|
@@ -7927,8 +8166,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
// Held in a local: the format outlives the call that reads it, and the slider and the
|
|
|
|
|
// range in its tooltip are then formatted the same way.
|
|
|
|
|
const std::string v2edge_format = auto_res ? _u8L("%.2f mm (auto)") : std::string("%.2f mm");
|
|
|
|
|
const bool v2edge_chosen = auto_res && rec.edge_mm > 0.f;
|
|
|
|
|
const wxString v2edge_tip =
|
|
|
|
|
auto_res && rec.edge_mm > 0.f ?
|
|
|
|
|
v2edge_chosen ?
|
|
|
|
|
from_u8(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)) :
|
|
|
|
@@ -7939,7 +8179,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
"back down and the detail goes with it.");
|
|
|
|
|
m_imgui->disabled_begin(busy || auto_res);
|
|
|
|
|
if (typed_slider("##v2edge", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, &shown, 0.02f, 2.f,
|
|
|
|
|
v2edge_format.c_str(), true, v2edge_tip)) {
|
|
|
|
|
v2edge_format.c_str(), true, v2edge_tip,
|
|
|
|
|
v2edge_chosen ? std::vector<TipMedia>{} : std::vector<TipMedia>{ { "subdivide.gif" } })) {
|
|
|
|
|
opts.v2_refine_mm = shown;
|
|
|
|
|
m_preview_params_dirty = true;
|
|
|
|
|
m_parent.set_as_dirty();
|
|
|
|
@@ -7981,7 +8222,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
&m_subdivide_budget_k, 10, 2000, "+%d k", true,
|
|
|
|
|
_L("How many thousand triangles the refinement may add to the model. The triangle "
|
|
|
|
|
"that fits worst is always split first, so even a run that spends the lot has "
|
|
|
|
|
"spent it where it shows most. Raise it if the relief still looks coarse."))) {
|
|
|
|
|
"spent it where it shows most. Raise it if the relief still looks coarse."),
|
|
|
|
|
{ { "subdivide.gif" } })) {
|
|
|
|
|
if (m_subdivide_editing)
|
|
|
|
|
rebuild_subdivide_preview();
|
|
|
|
|
m_parent.set_as_dirty();
|
|
|
|
@@ -8038,18 +8280,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
|
|
|
|
|
m_imgui->pop_bold_font();
|
|
|
|
|
GLGizmoUtils::pop_orca_button_style();
|
|
|
|
|
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled))
|
|
|
|
|
m_imgui->tooltip(mv != nullptr && !mv->is_texture_displacement_painted() ?
|
|
|
|
|
(m_seam_edit_mode ? _u8L("Nothing is painted yet. The UV editor's seam tool is on, so "
|
|
|
|
|
"strokes on the model mark seams instead of painting - turn "
|
|
|
|
|
"it off in the pane to paint.") :
|
|
|
|
|
_u8L("Nothing is painted yet.")) :
|
|
|
|
|
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.") :
|
|
|
|
|
_u8L("Turn the painted height maps into real geometry. The mesh is remeshed to an even "
|
|
|
|
|
"density and refined where the texture bends first, so the detail has vertices to "
|
|
|
|
|
"land on - all in one step."),
|
|
|
|
|
wrap_w);
|
|
|
|
|
panel_tooltip(mv != nullptr && !mv->is_texture_displacement_painted() ?
|
|
|
|
|
(m_seam_edit_mode ? _u8L("Nothing is painted yet. The UV editor's seam tool is on, so "
|
|
|
|
|
"strokes on the model mark seams instead of painting - turn "
|
|
|
|
|
"it off in the pane to paint.") :
|
|
|
|
|
_u8L("Nothing is painted yet.")) :
|
|
|
|
|
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.") :
|
|
|
|
|
_u8L("Turn the painted height maps into real geometry. The mesh is remeshed to an even "
|
|
|
|
|
"density and refined where the texture bends first, so the detail has vertices to "
|
|
|
|
|
"land on - all in one step."));
|
|
|
|
|
|
|
|
|
|
ImGui::SameLine();
|
|
|
|
|
if (busy) {
|
|
|
|
|