diff --git a/docs/HLSD/tooltip-illustrations.md b/docs/HLSD/tooltip-illustrations.md index c150d3ac2f..1d028398c3 100644 --- a/docs/HLSD/tooltip-illustrations.md +++ b/docs/HLSD/tooltip-illustrations.md @@ -61,16 +61,28 @@ for the next through `GLCanvas3D::schedule_extra_frame()`, timed to when that fr ## Making one Captures of the real tool are the first choice for anything involving the model, the mesh or colour. -Settings whose meaning is a shape can be drawn from the same arithmetic instead: -`scripts/gen_texture_displacement_tips.py` shades a height field as a relief catching the light, and -produces `tile_off.png`, `tile_on.png` and `rotation.gif` that way. +Settings whose meaning is a shape are drawn from the same arithmetic instead, by +`scripts/gen_texture_displacement_tips.py`: it shades a height field as a relief catching the light, +and covers Depth, Tile, Tile size, Rotation, Repeat/Mirror, Midlevel, Smoothing, Edge fade, Invert and +Blend that way. + +One motif - a ring inside a square frame - runs through nearly all of them, because using one shape +everywhere is what lets two pictures be compared: whatever differs between them is the setting, since +nothing else is. + +Midlevel is drawn in section rather than as a relief, and the reason generalises. Shading reads +slopes, so a height field moved up or down as a whole shades identically to where it started; any +setting whose meaning is *where the relief sits relative to the surface* is invisible in a top view +and has to be drawn from the side, against the bare surface as a reference. Either way: -- Square, around 256 px. The tooltip is about 20 em wide, so that is roughly native size on a 100% - display and downscales cleanly above it. +- Square, around 256 px (192 for an animation, where every frame ships). The tooltip is about 20 em + wide, so that is roughly native size on a 100% display and downscales cleanly above it. A single + picture is capped at 10 em tall and centred, two side by side take half the width each. - Neutral grey. These say what a shape does; colour in one reads as a third thing the setting does, and has to work against both themes. -- Animations loop seamlessly and stay short - a dozen frames at 100 ms, under about 250 KB. They sit - in `resources/`, which ships with every copy of the application. +- Animations loop seamlessly and stay short - 16 frames at 100 ms, under about 150 KB. They sit in + `resources/`, which ships with every copy of the application. A sweep driven by a cosine over the + frame count loops with no step, since it starts and ends at the same value and the same rate. - No text in the picture. Captions are translated; pixels are not. diff --git a/resources/images/texture_displacement/blend_add.png b/resources/images/texture_displacement/blend_add.png new file mode 100644 index 0000000000..e1d836167e Binary files /dev/null and b/resources/images/texture_displacement/blend_add.png differ diff --git a/resources/images/texture_displacement/blend_multiply.png b/resources/images/texture_displacement/blend_multiply.png new file mode 100644 index 0000000000..eea49dd12b Binary files /dev/null and b/resources/images/texture_displacement/blend_multiply.png differ diff --git a/resources/images/texture_displacement/depth.gif b/resources/images/texture_displacement/depth.gif new file mode 100644 index 0000000000..9b8e19b08a Binary files /dev/null and b/resources/images/texture_displacement/depth.gif differ diff --git a/resources/images/texture_displacement/edge_fade_off.png b/resources/images/texture_displacement/edge_fade_off.png new file mode 100644 index 0000000000..4652e38ba7 Binary files /dev/null and b/resources/images/texture_displacement/edge_fade_off.png differ diff --git a/resources/images/texture_displacement/edge_fade_on.png b/resources/images/texture_displacement/edge_fade_on.png new file mode 100644 index 0000000000..2abb2029aa Binary files /dev/null and b/resources/images/texture_displacement/edge_fade_on.png differ diff --git a/resources/images/texture_displacement/invert_off.png b/resources/images/texture_displacement/invert_off.png new file mode 100644 index 0000000000..65248727de Binary files /dev/null and b/resources/images/texture_displacement/invert_off.png differ diff --git a/resources/images/texture_displacement/invert_on.png b/resources/images/texture_displacement/invert_on.png new file mode 100644 index 0000000000..0944c0e120 Binary files /dev/null and b/resources/images/texture_displacement/invert_on.png differ diff --git a/resources/images/texture_displacement/midlevel.gif b/resources/images/texture_displacement/midlevel.gif new file mode 100644 index 0000000000..fdaad9356a Binary files /dev/null and b/resources/images/texture_displacement/midlevel.gif differ diff --git a/resources/images/texture_displacement/rotation.gif b/resources/images/texture_displacement/rotation.gif index 590b48eda3..94dfe64825 100644 Binary files a/resources/images/texture_displacement/rotation.gif and b/resources/images/texture_displacement/rotation.gif differ diff --git a/resources/images/texture_displacement/smoothing_off.png b/resources/images/texture_displacement/smoothing_off.png new file mode 100644 index 0000000000..b7f66fd330 Binary files /dev/null and b/resources/images/texture_displacement/smoothing_off.png differ diff --git a/resources/images/texture_displacement/smoothing_on.png b/resources/images/texture_displacement/smoothing_on.png new file mode 100644 index 0000000000..18ed9e00fd Binary files /dev/null and b/resources/images/texture_displacement/smoothing_on.png differ diff --git a/resources/images/texture_displacement/tile_mirror.png b/resources/images/texture_displacement/tile_mirror.png new file mode 100644 index 0000000000..0da432ff9d Binary files /dev/null and b/resources/images/texture_displacement/tile_mirror.png differ diff --git a/resources/images/texture_displacement/tile_repeat.png b/resources/images/texture_displacement/tile_repeat.png new file mode 100644 index 0000000000..ca9793897b Binary files /dev/null and b/resources/images/texture_displacement/tile_repeat.png differ diff --git a/resources/images/texture_displacement/tile_size_large.png b/resources/images/texture_displacement/tile_size_large.png new file mode 100644 index 0000000000..4bc2659244 Binary files /dev/null and b/resources/images/texture_displacement/tile_size_large.png differ diff --git a/resources/images/texture_displacement/tile_size_small.png b/resources/images/texture_displacement/tile_size_small.png new file mode 100644 index 0000000000..11b5bc84e7 Binary files /dev/null and b/resources/images/texture_displacement/tile_size_small.png differ diff --git a/scripts/gen_texture_displacement_tips.py b/scripts/gen_texture_displacement_tips.py index 1fbe9aff01..35ffe41562 100644 --- a/scripts/gen_texture_displacement_tips.py +++ b/scripts/gen_texture_displacement_tips.py @@ -2,11 +2,14 @@ """Draws the illustrations the texture-displacement tooltips show. The panel's tooltips can carry a picture under their text (see GLGizmoTextureDisplacement's -panel_tooltip()). A few of those settings - the ones that are a shape rather than a photograph - +panel_tooltip()). A good many of those settings - the ones that are a shape rather than a photograph - are better drawn from the same arithmetic the feature uses than captured from the screen, which is what this script does: it shades a height field exactly as a relief would catch the light, so "what Rotation turns" and "what Tile repeats" are visible rather than described. +Settings that involve the model, the mesh or colour are not here: those want a capture of the real +tool. See docs/HLSD/tooltip-illustrations.md. + Output goes to resources/images/texture_displacement/. Run it after changing one of the patterns below; nothing in the build depends on it. @@ -21,6 +24,7 @@ import numpy as np from PIL import Image SIZE = 256 # px, square - the panel draws these at whatever width the tooltip has +GIF_SIZE = 192 # animations are the same picture, smaller: every frame of one ships in resources/ RELIEF = 30.0 # how hard the shading reacts to a slope; purely cosmetic OUT_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "resources", "images", "texture_displacement") @@ -41,8 +45,10 @@ def grid(size=SIZE): def shade(height): - """A height field in 0..1, lit as a relief and tinted: the picture the tooltip shows.""" - gy, gx = np.gradient(height * RELIEF) + """A height field in 0..1, lit as a relief: the picture the tooltip shows.""" + # The slope has to mean the same thing at either size, so the gradient is taken per unit of the + # picture rather than per pixel - otherwise the smaller animations would come out twice as flat. + gy, gx = np.gradient(height * RELIEF * (height.shape[0] / float(SIZE))) normal = np.dstack([-gx, -gy, np.ones_like(height)]) normal /= np.linalg.norm(normal, axis=2, keepdims=True) lam = np.clip(normal @ LIGHT, 0.0, 1.0) @@ -72,50 +78,151 @@ def vignette(rgb): def save_png(name, rgb): path = os.path.join(OUT_DIR, name) Image.fromarray(vignette(rgb)).save(path, optimize=True) - print("wrote", os.path.normpath(path)) + print("wrote", os.path.normpath(path), os.path.getsize(path) // 1024, "KB") def save_gif(name, frames, delay_ms): path = os.path.join(OUT_DIR, name) images = [Image.fromarray(vignette(f)).convert("P", palette=Image.ADAPTIVE, colors=32) for f in frames] images[0].save(path, save_all=True, append_images=images[1:], duration=delay_ms, loop=0, optimize=True) - print("wrote", os.path.normpath(path)) + print("wrote", os.path.normpath(path), os.path.getsize(path) // 1024, "KB") + + +def smoothstep(edge0, edge1, x): + t = np.clip((x - edge0) / (edge1 - edge0), 0.0, 1.0) + return t * t * (3.0 - 2.0 * t) + + +def blur(a, radius): + """Separable box blur, run twice - near enough a gaussian for a picture of a blur.""" + for _ in range(2): + for axis in (0, 1): + padded = np.pad(a, [(radius, radius) if i == axis else (0, 0) for i in range(2)], mode="edge") + cumulative = np.cumsum(padded, axis=axis) + cumulative = np.concatenate([np.zeros_like(np.take(cumulative, [0], axis=axis)), cumulative], axis=axis) + n = a.shape[axis] + hi = np.take(cumulative, np.arange(n) + 2 * radius + 1, axis=axis) + lo = np.take(cumulative, np.arange(n), axis=axis) + a = (hi - lo) / (2.0 * radius + 1.0) + return a # --- the patterns ------------------------------------------------------------------------------ +# +# One motif runs through nearly all of them - a ring inside a square frame. Using the same shape for +# every setting is what lets the pictures be compared: whatever changes between two of them is the +# setting, because nothing else is different. -def ridges(angle_deg, period=0.16): - """Parallel ridges at an angle: the simplest pattern whose direction is unmistakable.""" - x, y = grid() - a = np.radians(angle_deg) - t = (x * np.cos(a) + y * np.sin(a)) / period - return 0.2 + 0.6 * (0.5 + 0.5 * np.sin(2.0 * np.pi * t)) - - -def motif(repeats): - """A ring inside a square frame, laid out `repeats` times across the picture.""" - x, y = grid() - u = (x * repeats) % 1.0 - 0.5 - v = (y * repeats) % 1.0 - 0.5 +def _motif_cell(u, v): + """The motif within one cell, u and v in -0.5 .. 0.5.""" r = np.hypot(u, v) - ring = np.clip(1.0 - np.abs(r - 0.26) / 0.085, 0.0, 1.0) frame_d = np.maximum(np.abs(u), np.abs(v)) frame = np.clip(1.0 - np.abs(frame_d - 0.42) / 0.055, 0.0, 1.0) return np.clip(np.maximum(ring, frame) ** 0.7, 0.0, 1.0) +def motif(repeats, size=SIZE): + """The motif laid out `repeats` times across the picture.""" + x, y = grid(size) + return _motif_cell((x * repeats) % 1.0 - 0.5, (y * repeats) % 1.0 - 0.5) + + def decal(motif_scale=0.46): """One copy of the motif in the middle of a flat field - what Tile off leaves.""" x, y = grid() u, v = x / motif_scale, y / motif_scale - r = np.hypot(u, v) - ring = np.clip(1.0 - np.abs(r - 0.26) / 0.085, 0.0, 1.0) - frame_d = np.maximum(np.abs(u), np.abs(v)) - frame = np.clip(1.0 - np.abs(frame_d - 0.42) / 0.055, 0.0, 1.0) inside = (np.maximum(np.abs(u), np.abs(v)) <= 0.5).astype(float) - return np.clip(np.maximum(ring, frame) ** 0.7, 0.0, 1.0) * inside + return _motif_cell(u, v) * inside + + +def ridges(angle_deg, period=0.16, size=SIZE): + """Parallel ridges at an angle: the simplest pattern whose direction is unmistakable.""" + x, y = grid(size) + a = np.radians(angle_deg) + t = (x * np.cos(a) + y * np.sin(a)) / period + return 0.2 + 0.6 * (0.5 + 0.5 * np.sin(2.0 * np.pi * t)) + + +def bosses(repeats, size=SIZE): + """Rounded domes standing on a flat field - a shape with an unmistakable right way up.""" + x, y = grid(size) + u = (x * repeats) % 1.0 - 0.5 + v = (y * repeats) % 1.0 - 0.5 + return smoothstep(0.36, 0.08, np.hypot(u, v)) + + +def chevron_tiles(mirrored, repeats=3): + """Cells of diagonal ridges, laid out repeated or mirrored. + + Repeated, every cell starts the pattern over and the seam between two of them is a step; mirrored, + each cell is the reflection of its neighbour and the ridges meet across the seam as chevrons. That + seam is the whole of what the Repeat/Mirror pair does, and a motif symmetric in itself would hide + it entirely. + """ + + def axis(t): + if not mirrored: + return (t * repeats) % 1.0 + s = (t * repeats) % 2.0 + return np.where(s < 1.0, s, 2.0 - s) + + x, y = grid() + t = (axis(x + 0.5) + axis(y + 0.5)) * 1.5 + return 0.22 + 0.56 * (0.5 + 0.5 * np.cos(2.0 * np.pi * t)) + + +def painted_area(size=SIZE, radius=0.44): + """A round painted area in the middle of bare surface, with a soft edge so it does not alias.""" + x, y = grid(size) + return smoothstep(radius, radius - 1.5 / size, np.hypot(x, y)) + + +def painted_patch(fade, size=SIZE): + """The motif inside a painted area, either stopping at its edge or fading into it.""" + x, y = grid(size) + r = np.hypot(x, y) / 0.44 + amount = smoothstep(1.0, 0.55, r) if fade else 1.0 + return motif(3, size) * painted_area(size) * amount + + +def profile_wave(size=SIZE, periods=2.5): + """A one-dimensional height profile with clear highs, lows and a flat stretch between them.""" + t = (np.arange(size) + 0.5) / size * periods * 2.0 * np.pi + h = 0.5 + 0.34 * np.cos(t) + 0.16 * np.cos(2.0 * t + 0.9) + return np.clip((h - h.min()) / (h.max() - h.min()), 0.0, 1.0) + + +def profile_view(height_1d, midlevel, amplitude=0.26, size=SIZE): + """A side view of the surface: material below, air above, and the bare surface as a dashed line. + + Shading cannot say how high something is - it reads slopes, and a height field moved up or down as + a whole has exactly the slopes it had before. Midlevel is entirely about where the texture sits + relative to the surface it is on, so it is drawn in section, where that is the one thing visible. + """ + base = 0.52 * size + surface = base - (height_1d - midlevel) * amplitude * size + rows = np.arange(size)[:, None] + + covered = np.clip(rows - surface[None, :] + 0.5, 0.0, 1.0) # 1 where there is material + air = np.array([0.11, 0.12, 0.13]) + material = np.array([0.44, 0.46, 0.47]) + rgb = air * (1.0 - covered)[..., None] + material * covered[..., None] + + # The lit top face, and the bare surface the texture is measured against. + edge = np.clip(1.0 - np.abs(rows - surface[None, :]) / 1.6, 0.0, 1.0) + rgb = rgb * (1.0 - edge[..., None]) + np.array([0.80, 0.86, 0.85]) * edge[..., None] + dash = ((np.arange(size) // 7) % 2 == 0)[None, :] + line = (np.abs(rows - base) < 0.9) & dash + rgb[line] = np.array([0.58, 0.62, 0.62]) + return (np.clip(rgb, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8) + + +def dome(size=SIZE): + """One broad swell filling the picture - the layer underneath, for the blend pictures.""" + x, y = grid(size) + return smoothstep(0.52, 0.02, np.hypot(x, y)) def main(): @@ -123,13 +230,63 @@ def main(): # Tile: one copy against the pattern filling the painted area. save_png("tile_off.png", shade(decal())) - save_png("tile_on.png", shade(motif(repeats=3))) + save_png("tile_on.png", shade(motif(3))) - # Rotation: the same ridges turning a quarter of a turn and back, so the loop has no jump. - # A half turn brings parallel ridges back onto themselves, so a single sweep loops with no jump - # and the file stays a fraction of the size a there-and-back one would be. + # Tile size: how wide one copy is. + save_png("tile_size_large.png", shade(motif(2))) + save_png("tile_size_small.png", shade(motif(5))) + + # Repeat against Mirror, where the seam between two copies is the whole point. + save_png("tile_repeat.png", shade(chevron_tiles(mirrored=False))) + save_png("tile_mirror.png", shade(chevron_tiles(mirrored=True))) + + # Invert: the negative of the image. Domes rather than the ring motif, which is close enough to + # its own negative that inverting it would look like nothing had happened. + save_png("invert_off.png", shade(bosses(3))) + save_png("invert_on.png", shade(1.0 - bosses(3))) + + # Smoothing: a stepped image against the same one blurred before it is used. Domes again, where + # the steps show up as contour rings - what a photograph with too few grey levels does. + stepped = np.floor(bosses(3) * 5.0) / 4.0 + save_png("smoothing_off.png", shade(np.clip(stepped, 0.0, 1.0))) + save_png("smoothing_on.png", shade(np.clip(blur(stepped, max(2, SIZE // 40)), 0.0, 1.0))) + + # Edge fade: relief that stops at the edge of the painted area against relief that sinks into it. + save_png("edge_fade_off.png", shade(painted_patch(fade=False))) + save_png("edge_fade_on.png", shade(painted_patch(fade=True))) + + # Blend: piling relief on top of the layer below against using it as a mask over it. Add keeps + # both shapes, Multiply keeps the motif only where the layer under it is high. + base = dome() + save_png("blend_add.png", shade(np.clip(0.55 * base + 0.42 * motif(3), 0.0, 1.0))) + save_png("blend_multiply.png", shade(base * motif(3))) + + # Rotation: a half turn brings parallel ridges back onto themselves, so a single sweep loops with + # no jump and the file stays a fraction of the size a there-and-back one would be. steps = 16 - save_gif("rotation.gif", [shade(ridges(180.0 * i / steps)) for i in range(steps)], delay_ms=100) + save_gif("rotation.gif", [shade(ridges(180.0 * i / steps, size=GIF_SIZE)) for i in range(steps)], delay_ms=100) + + # Depth and Midlevel are both about where the relief sits relative to the surface it is on, so + # both are drawn as a painted area in the middle of bare surface, which is that reference. Without + # it neither would show: shading reads slopes, and a height field that moves up or down as a whole + # has exactly the same slopes it had before. + # + # A cosine over the frame count starts and ends at the same value with no step in between, so a + # single pass through it loops. + area = painted_area(GIF_SIZE) + pattern = motif(3, GIF_SIZE) + sweep = [0.5 - 0.5 * np.cos(2.0 * np.pi * i / steps) for i in range(steps)] + + # Depth: the relief growing out of the surface and settling back flush with it. + frames = [shade(np.clip(0.5 + 0.45 * pattern * (0.08 + 0.92 * t) * area, 0.0, 1.0)) for t in sweep] + save_gif("depth.gif", frames, delay_ms=100) + + # Midlevel: the grey that stays put, sweeping from "the whole texture pushes out" through + # "mid-grey stays where the surface is" to "the whole of it cuts in". Drawn in section - see + # profile_view() for why this one is not a relief like the rest. + wave = profile_wave(GIF_SIZE) + frames = [profile_view(wave, t, size=GIF_SIZE) for t in sweep] + save_gif("midlevel.gif", frames, delay_ms=100) if __name__ == "__main__": diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 0339806381..0a976f5dd7 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -3768,16 +3768,28 @@ void GLGizmoTextureDisplacement::render_tip_media(const std::vector &m 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(); double next = 0.; const unsigned int id = texture->texture_id(time, &next); - const float h = std::round(cell_w * float(texture->height()) / float(texture->width())); - ImGui::Image((ImTextureID) (intptr_t) id, ImVec2(cell_w, h)); + 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 - and asks for exactly when its // next frame is due, rather than for a redraw as fast as the machine will go. @@ -3785,10 +3797,9 @@ void GLGizmoTextureDisplacement::render_tip_media(const std::vector &m m_parent.schedule_extra_frame(int(next * 1000.0)); if (!entry.caption.empty()) { - // Centred under its own illustration, and never wider than it. + // 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; - const float cell_x = ImGui::GetCursorPosX(); 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()); @@ -6565,11 +6576,13 @@ 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); 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.")); + "millimetres. Black does not move it at all, unless Midlevel says otherwise."), + { { "depth.gif" } }); 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 " - "single copy.")); + "single copy."), + { { "tile_size_large.png", _L("Larger") }, { "tile_size_small.png", _L("Smaller") } }); m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad); hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an " "edge of the model."), @@ -6593,7 +6606,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.")); @@ -6603,12 +6617,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); 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.")); + "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); ImGui::SetNextItemWidth(-card_pad); @@ -6623,7 +6639,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))); @@ -6820,14 +6837,14 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float 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(0), - icon_sm, _L("Repeat"), _L("Repeat"), tile_na)) { + icon_sm, _L("Repeat"), _L("Repeat"), tile_na, { { "tile_repeat.png" } })) { layer.tile_method = static_cast(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(1), icon_sm, _L("Mirrored repeat"), _L("Mirrored repeat - every other tile is flipped, so edges meet seamlessly"), - tile_na)) { + tile_na, { { "tile_mirror.png" } })) { layer.tile_method = static_cast(1); m_preview_params_dirty = true; } @@ -6863,7 +6880,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "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.")); + "untouched."), + base_layer ? std::vector{} : + std::vector{ { "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"),