diff --git a/docs/HLSD/tooltip-illustrations.md b/docs/HLSD/tooltip-illustrations.md index 1d028398c3..69f7cc717a 100644 --- a/docs/HLSD/tooltip-illustrations.md +++ b/docs/HLSD/tooltip-illustrations.md @@ -55,25 +55,45 @@ so an animation authored against that plays at the speed it was authored at. The animation restarts when its tooltip opens, rather than running off a free-running clock: these are short loops of one setting going from one value to another, and joining one halfway through shows the -end of a change the user never saw the start of. The canvas only redraws on demand, so each frame asks -for the next through `GLCanvas3D::schedule_extra_frame()`, timed to when that frame is actually due. +end of a change the user never saw the start of. + +Driving the frames takes two calls, not one. `GLCanvas3D::schedule_extra_frame()` only guarantees an +idle event at the moment the next frame is due; `GLCanvas3D::on_idle()` returns without rendering +anything unless something has marked the canvas dirty by then, so an animation left to the timer alone +advances one frame per mouse movement and stands still whenever the mouse does. `set_dirty()` on the +gizmo is what on_idle reads, through `GLGizmoBase::update_items_state()`, to find out there are frames +still to draw. The pair of them is what makes an animation play at its own speed and stop costing +anything the moment its tooltip closes. ## 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 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. +`scripts/gen_texture_displacement_tips.py` draws the lot, in four languages, picked by what the +setting actually is: -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. +- **A shaded relief**, for the settings that are about a pattern: Depth, Tile, Tile size, Rotation, + Repeat/Mirror, Smoothing, Edge fade, Invert, Blend and the Normal, Fast, Checker and Distortion + views. One motif - a ring inside a square frame - runs through 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. +- **A flat mesh of triangles**, for the settings that are about the mesh or about a tool: the paint + tools, brush size, the connected-area fill, subdivision, feature-adaptive refinement, remeshing, + hard edges, slivers and the wireframe view. These carry the one accent colour in the set, meaning + "painted", because that is the only thing in such a picture that is not geometry. +- **A section**, for Midlevel and for smoothing the geometry. The reason generalises: shading reads + slopes, so a height field moved up or down as a whole shades identically to where it started, and + any setting whose meaning is *where the surface sits relative to where it was* is invisible from + above and has to be drawn from the side. +- **A flat ramp of greys**, for Mix filaments, Mixed colors and Denoise. Those are about which + filament a spot ends up as; lighting one would only add a second thing changing between two + pictures meant to differ in one. -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. +The five Mapping pictures are one ball with a checker laid on it five ways, which is the whole of what +choosing a projection does - except Unwrap, which is not a projection onto the model at all and so is +drawn as the flat layout it produces. + +Captures of the real tool are still the first choice for anything involving a real model or real +filament colour: Bake, the mixed-colour count on a photograph, Adjust placement, the frame window and +the UV editor's seam tool have no picture yet for that reason. Either way: diff --git a/resources/images/texture_displacement/adaptive_off.png b/resources/images/texture_displacement/adaptive_off.png new file mode 100644 index 0000000000..c509ee5871 Binary files /dev/null and b/resources/images/texture_displacement/adaptive_off.png differ diff --git a/resources/images/texture_displacement/adaptive_on.png b/resources/images/texture_displacement/adaptive_on.png new file mode 100644 index 0000000000..43f2667457 Binary files /dev/null and b/resources/images/texture_displacement/adaptive_on.png differ diff --git a/resources/images/texture_displacement/collapse_slivers_off.png b/resources/images/texture_displacement/collapse_slivers_off.png new file mode 100644 index 0000000000..f76898339a Binary files /dev/null and b/resources/images/texture_displacement/collapse_slivers_off.png differ diff --git a/resources/images/texture_displacement/collapse_slivers_on.png b/resources/images/texture_displacement/collapse_slivers_on.png new file mode 100644 index 0000000000..3b1823c59c Binary files /dev/null and b/resources/images/texture_displacement/collapse_slivers_on.png differ diff --git a/resources/images/texture_displacement/cursor_size.gif b/resources/images/texture_displacement/cursor_size.gif new file mode 100644 index 0000000000..3c6e5a7d92 Binary files /dev/null and b/resources/images/texture_displacement/cursor_size.gif differ diff --git a/resources/images/texture_displacement/denoise_off.png b/resources/images/texture_displacement/denoise_off.png new file mode 100644 index 0000000000..4450932491 Binary files /dev/null and b/resources/images/texture_displacement/denoise_off.png differ diff --git a/resources/images/texture_displacement/denoise_on.png b/resources/images/texture_displacement/denoise_on.png new file mode 100644 index 0000000000..ff679e058a Binary files /dev/null and b/resources/images/texture_displacement/denoise_on.png differ diff --git a/resources/images/texture_displacement/depth.gif b/resources/images/texture_displacement/depth.gif index 9b8e19b08a..d7e9dd615e 100644 Binary files a/resources/images/texture_displacement/depth.gif and b/resources/images/texture_displacement/depth.gif differ diff --git a/resources/images/texture_displacement/erase.gif b/resources/images/texture_displacement/erase.gif new file mode 100644 index 0000000000..ef4a82e7b1 Binary files /dev/null and b/resources/images/texture_displacement/erase.gif differ diff --git a/resources/images/texture_displacement/erase_all.png b/resources/images/texture_displacement/erase_all.png new file mode 100644 index 0000000000..5633bdfa0a Binary files /dev/null and b/resources/images/texture_displacement/erase_all.png differ diff --git a/resources/images/texture_displacement/geometry_smoothing.gif b/resources/images/texture_displacement/geometry_smoothing.gif new file mode 100644 index 0000000000..ad55fbc09b Binary files /dev/null and b/resources/images/texture_displacement/geometry_smoothing.gif differ diff --git a/resources/images/texture_displacement/map_cube.png b/resources/images/texture_displacement/map_cube.png new file mode 100644 index 0000000000..cf50adfd7e Binary files /dev/null and b/resources/images/texture_displacement/map_cube.png differ diff --git a/resources/images/texture_displacement/map_cylinder.png b/resources/images/texture_displacement/map_cylinder.png new file mode 100644 index 0000000000..f9c2986fb2 Binary files /dev/null and b/resources/images/texture_displacement/map_cylinder.png differ diff --git a/resources/images/texture_displacement/map_sphere.png b/resources/images/texture_displacement/map_sphere.png new file mode 100644 index 0000000000..0a8dcbbc05 Binary files /dev/null and b/resources/images/texture_displacement/map_sphere.png differ diff --git a/resources/images/texture_displacement/map_unwrap.png b/resources/images/texture_displacement/map_unwrap.png new file mode 100644 index 0000000000..4caaf6bb83 Binary files /dev/null and b/resources/images/texture_displacement/map_unwrap.png differ diff --git a/resources/images/texture_displacement/map_view.png b/resources/images/texture_displacement/map_view.png new file mode 100644 index 0000000000..1f4714689d Binary files /dev/null and b/resources/images/texture_displacement/map_view.png differ diff --git a/resources/images/texture_displacement/midlevel.gif b/resources/images/texture_displacement/midlevel.gif index fdaad9356a..32914d7c9d 100644 Binary files a/resources/images/texture_displacement/midlevel.gif and b/resources/images/texture_displacement/midlevel.gif differ diff --git a/resources/images/texture_displacement/mix_off.png b/resources/images/texture_displacement/mix_off.png new file mode 100644 index 0000000000..d6445d6314 Binary files /dev/null and b/resources/images/texture_displacement/mix_off.png differ diff --git a/resources/images/texture_displacement/mix_on.png b/resources/images/texture_displacement/mix_on.png new file mode 100644 index 0000000000..716f63e0b9 Binary files /dev/null and b/resources/images/texture_displacement/mix_on.png differ diff --git a/resources/images/texture_displacement/mixed_colors_few.png b/resources/images/texture_displacement/mixed_colors_few.png new file mode 100644 index 0000000000..389e22f429 Binary files /dev/null and b/resources/images/texture_displacement/mixed_colors_few.png differ diff --git a/resources/images/texture_displacement/mixed_colors_many.png b/resources/images/texture_displacement/mixed_colors_many.png new file mode 100644 index 0000000000..90754062ce Binary files /dev/null and b/resources/images/texture_displacement/mixed_colors_many.png differ diff --git a/resources/images/texture_displacement/paint.gif b/resources/images/texture_displacement/paint.gif new file mode 100644 index 0000000000..c7fedc1a3b Binary files /dev/null and b/resources/images/texture_displacement/paint.gif differ diff --git a/resources/images/texture_displacement/painted_only_off.png b/resources/images/texture_displacement/painted_only_off.png new file mode 100644 index 0000000000..ac30fea631 Binary files /dev/null and b/resources/images/texture_displacement/painted_only_off.png differ diff --git a/resources/images/texture_displacement/painted_only_on.png b/resources/images/texture_displacement/painted_only_on.png new file mode 100644 index 0000000000..b8c01e1b13 Binary files /dev/null and b/resources/images/texture_displacement/painted_only_on.png differ diff --git a/resources/images/texture_displacement/preserve_edges_off.png b/resources/images/texture_displacement/preserve_edges_off.png new file mode 100644 index 0000000000..8eb41e7d8c Binary files /dev/null and b/resources/images/texture_displacement/preserve_edges_off.png differ diff --git a/resources/images/texture_displacement/preserve_edges_on.png b/resources/images/texture_displacement/preserve_edges_on.png new file mode 100644 index 0000000000..a8a8f5b7d6 Binary files /dev/null and b/resources/images/texture_displacement/preserve_edges_on.png differ diff --git a/resources/images/texture_displacement/remesh_after.png b/resources/images/texture_displacement/remesh_after.png new file mode 100644 index 0000000000..7b46db61ed Binary files /dev/null and b/resources/images/texture_displacement/remesh_after.png differ diff --git a/resources/images/texture_displacement/remesh_before.png b/resources/images/texture_displacement/remesh_before.png new file mode 100644 index 0000000000..8c6f56c3d9 Binary files /dev/null and b/resources/images/texture_displacement/remesh_before.png differ diff --git a/resources/images/texture_displacement/rotation.gif b/resources/images/texture_displacement/rotation.gif index 94dfe64825..c255e5c167 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/select_all.png b/resources/images/texture_displacement/select_all.png new file mode 100644 index 0000000000..c727daf7fa Binary files /dev/null and b/resources/images/texture_displacement/select_all.png differ diff --git a/resources/images/texture_displacement/smart_fill_angle_high.png b/resources/images/texture_displacement/smart_fill_angle_high.png new file mode 100644 index 0000000000..584a8468ec Binary files /dev/null and b/resources/images/texture_displacement/smart_fill_angle_high.png differ diff --git a/resources/images/texture_displacement/smart_fill_angle_low.png b/resources/images/texture_displacement/smart_fill_angle_low.png new file mode 100644 index 0000000000..88a6c9612f Binary files /dev/null and b/resources/images/texture_displacement/smart_fill_angle_low.png differ diff --git a/resources/images/texture_displacement/subdivide.gif b/resources/images/texture_displacement/subdivide.gif new file mode 100644 index 0000000000..70d8abf47a Binary files /dev/null and b/resources/images/texture_displacement/subdivide.gif differ diff --git a/resources/images/texture_displacement/tool_face.gif b/resources/images/texture_displacement/tool_face.gif new file mode 100644 index 0000000000..be616fdd52 Binary files /dev/null and b/resources/images/texture_displacement/tool_face.gif differ diff --git a/resources/images/texture_displacement/tool_smart_fill.gif b/resources/images/texture_displacement/tool_smart_fill.gif new file mode 100644 index 0000000000..1dc23ef8c6 Binary files /dev/null and b/resources/images/texture_displacement/tool_smart_fill.gif differ diff --git a/resources/images/texture_displacement/view_checker.png b/resources/images/texture_displacement/view_checker.png new file mode 100644 index 0000000000..0198b8581c Binary files /dev/null and b/resources/images/texture_displacement/view_checker.png differ diff --git a/resources/images/texture_displacement/view_distortion.png b/resources/images/texture_displacement/view_distortion.png new file mode 100644 index 0000000000..d8be4b187c Binary files /dev/null and b/resources/images/texture_displacement/view_distortion.png differ diff --git a/resources/images/texture_displacement/view_fast.png b/resources/images/texture_displacement/view_fast.png new file mode 100644 index 0000000000..71ed3d1953 Binary files /dev/null and b/resources/images/texture_displacement/view_fast.png differ diff --git a/resources/images/texture_displacement/view_wireframe.png b/resources/images/texture_displacement/view_wireframe.png new file mode 100644 index 0000000000..6585274de9 Binary files /dev/null and b/resources/images/texture_displacement/view_wireframe.png differ diff --git a/scripts/gen_texture_displacement_tips.py b/scripts/gen_texture_displacement_tips.py index 35ffe41562..6bf7e3bdf6 100644 --- a/scripts/gen_texture_displacement_tips.py +++ b/scripts/gen_texture_displacement_tips.py @@ -21,7 +21,7 @@ Needs numpy and pillow. import os import numpy as np -from PIL import Image +from PIL import Image, ImageDraw 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/ @@ -83,7 +83,7 @@ def save_png(name, rgb): 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 = [Image.fromarray(vignette(f)).convert("P", palette=Image.ADAPTIVE, colors=24) 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), os.path.getsize(path) // 1024, "KB") @@ -225,6 +225,404 @@ def dome(size=SIZE): return smoothstep(0.52, 0.02, np.hypot(x, y)) +# --- meshes, cursors and sections --------------------------------------------------------------- +# +# The second family of pictures. The settings above are about a pattern, and a shaded relief is what a +# pattern looks like; these are about a tool or about the mesh, where what matters is which triangles +# exist and which of them are painted. So they are drawn flat, the way the panel's own wireframe and +# paint overlays draw them, with one accent colour meaning "painted" and nothing else coloured. + +SS = 3 # supersampling: PIL fills polygons without antialiasing, so draw big and shrink + +INK = (0.62, 0.67, 0.67) # mesh edges +BARE = (0.28, 0.30, 0.31) # unpainted surface +PAINT = (0.20, 0.47, 0.45) # painted with the active layer, in the panel's own accent +CURSOR = (0.92, 0.95, 0.95) +MARGIN = 0.05 # keeps the mesh off the picture's border, where the vignette darkens it + + +def _px(p, size): + """Unit coordinates to pixels, inside the margin.""" + return ((MARGIN + p[0] * (1.0 - 2.0 * MARGIN)) * size, (MARGIN + p[1] * (1.0 - 2.0 * MARGIN)) * size) + + +def _rgb255(c): + return tuple(int(round(v * 255.0)) for v in c) + + +def raster(draw_fn, size=SIZE, background=BARE): + """Runs draw_fn(draw, size_in_pixels) on a supersampled canvas and hands back the shrunk result. + + `background` is a colour, or a picture already drawn by the relief half of this script - which is + how a mesh is shown sitting on the surface it refines. + """ + if isinstance(background, np.ndarray): + image = Image.fromarray(background).resize((size * SS, size * SS), Image.BILINEAR) + else: + image = Image.new("RGB", (size * SS, size * SS), _rgb255(background)) + draw_fn(ImageDraw.Draw(image), size * SS) + return np.asarray(image.resize((size, size), Image.LANCZOS)).astype(np.uint8) + + +def tri_mesh(n, jitter=0.0, seed=1): + """A square patch of triangles, optionally with its inner vertices nudged off the grid. + + Jitter is not decoration: a perfectly regular grid reads as graph paper, and the point of these + pictures is a mesh over a surface. + """ + rng = np.random.default_rng(seed) + v = np.stack(np.meshgrid(np.linspace(0.0, 1.0, n + 1), np.linspace(0.0, 1.0, n + 1)), axis=-1) + if jitter: + v[1:n, 1:n] += (rng.random((n - 1, n - 1, 2)) - 0.5) * (jitter / n) + tris = [] + for i in range(n): + for j in range(n): + tris.append((v[i, j], v[i, j + 1], v[i + 1, j + 1])) + tris.append((v[i, j], v[i + 1, j + 1], v[i + 1, j])) + return tris + + +def centroids(tris): + return np.array([np.mean(t, axis=0) for t in tris]) + + +def neighbours(tris): + """Which triangles share an edge with which, keyed on the rounded vertex positions.""" + edges = {} + for i, t in enumerate(tris): + for a, b in ((0, 1), (1, 2), (2, 0)): + key = tuple(sorted((tuple(np.round(t[a], 6)), tuple(np.round(t[b], 6))))) + edges.setdefault(key, []).append(i) + out = [[] for _ in tris] + for shared in edges.values(): + if len(shared) == 2: + out[shared[0]].append(shared[1]) + out[shared[1]].append(shared[0]) + return out + + +def draw_mesh(draw, size, tris, painted=None, edge=INK, edge_width=0.7): + fill_bare, fill_paint = _rgb255(BARE), _rgb255(PAINT) + for i, t in enumerate(tris): + draw.polygon([_px(p, size) for p in t], + fill=fill_paint if painted is not None and painted[i] else fill_bare, + outline=_rgb255(edge), width=max(1, int(edge_width * SS))) + + +def draw_brush(draw, size, centre, radius, filled=False): + """The brush cursor: a ring, which is how the gizmo draws it on the model.""" + c = _px(centre, size) + r = radius * (1.0 - 2.0 * MARGIN) * size + if filled: + draw.ellipse([c[0] - r, c[1] - r, c[0] + r, c[1] + r], fill=(255, 255, 255, 40)) + draw.ellipse([c[0] - r, c[1] - r, c[0] + r, c[1] + r], outline=_rgb255(CURSOR), width=max(1, int(1.6 * SS))) + + +def draw_pointer(draw, size, at): + """A small arrow, for the tools that are clicked rather than dragged.""" + x, y = _px(at, size) + k = 0.055 * size + draw.polygon([(x, y), (x, y + 1.45 * k), (x + 0.38 * k, y + 1.05 * k), + (x + 0.62 * k, y + 1.55 * k), (x + 0.90 * k, y + 1.40 * k), + (x + 0.66 * k, y + 0.92 * k), (x + 1.05 * k, y + 0.84 * k)], + fill=_rgb255(CURSOR), outline=(30, 32, 34), width=max(1, int(0.9 * SS))) + + +def stroke_path(t): + """The path a demonstrated brush stroke follows, t in 0..1.""" + return np.array([0.14 + 0.72 * t, 0.46 + 0.22 * np.sin(2.0 * np.pi * t * 1.1)]) + + +def brush_frames(size, erase, steps=14, radius=0.115, hold=3): + tris = tri_mesh(9, jitter=0.55) + cents = centroids(tris) + painted = np.ones(len(tris), bool) if erase else np.zeros(len(tris), bool) + frames = [] + for k in range(steps): + centre = stroke_path(k / (steps - 1.0)) + hit = np.linalg.norm(cents - centre, axis=1) < radius + painted = painted & ~hit if erase else painted | hit + state = painted.copy() + frames.append(raster(lambda d, s, st=state, c=centre: (draw_mesh(d, s, tris, st), draw_brush(d, s, c, radius)), size)) + frames += [frames[-1]] * hold + return frames + + +def face_frames(size, steps=9, hold=3): + """The Face tool: one triangle per click, under a pointer.""" + tris = tri_mesh(7, jitter=0.5, seed=4) + cents = centroids(tris) + order = np.argsort(np.linalg.norm(cents - np.array([0.5, 0.5]), axis=1))[:steps] + painted = np.zeros(len(tris), bool) + frames = [] + for i in order: + painted[i] = True + state = painted.copy() + at = cents[i] + frames.append(raster(lambda d, s, st=state, a=at: (draw_mesh(d, s, tris, st), draw_pointer(d, s, a)), size)) + frames += [frames[-1]] * hold + return frames + + +def flood_frames(size, creases, steps=12, hold=3, seed=7): + """The Connected area tool: a flood from one click, stopped by the creases it may not cross. + + `creases` is a list of x positions; a triangle pair straddling one cannot pass the fill between + them, which is what an edge sharper than the angle threshold does on the model. + """ + tris = tri_mesh(9, jitter=0.5, seed=seed) + cents = centroids(tris) + adj = neighbours(tris) + blocked = lambda i, j: any((cents[i][0] - x) * (cents[j][0] - x) < 0.0 for x in creases) + + seed_tri = int(np.argmin(np.linalg.norm(cents - np.array([0.22, 0.5]), axis=1))) + painted = np.zeros(len(tris), bool) + painted[seed_tri] = True + frontier = [seed_tri] + frames = [] + for _ in range(steps): + state = painted.copy() + frames.append(raster(lambda d, s, st=state: (draw_mesh(d, s, tris, st), + draw_creases(d, s, creases), + draw_pointer(d, s, cents[seed_tri])), size)) + nxt = [] + for i in frontier: + for j in adj[i]: + if not painted[j] and not blocked(i, j): + painted[j] = True + nxt.append(j) + frontier = nxt + frames += [frames[-1]] * hold + return frames + + +def draw_creases(draw, size, creases): + """A fold too sharp for the fill to cross: drawn dark with a bright crest, like a real one.""" + for x in creases: + a, b = _px((x, -0.02), size), _px((x, 1.02), size) + draw.line([a, b], fill=_rgb255((0.08, 0.09, 0.10)), width=max(1, int(4.0 * SS))) + draw.line([a, b], fill=_rgb255((0.97, 0.99, 0.99)), width=max(1, int(1.4 * SS))) + + + +def split4(tri): + a, b, c = tri + ab, bc, ca = (a + b) * 0.5, (b + c) * 0.5, (c + a) * 0.5 + return [(a, ab, ca), (ab, b, bc), (ca, bc, c), (ab, bc, ca)] + + +def refine(tris, density, max_depth, threshold=0.45): + """Splits a triangle into four wherever `density` at its middle asks for it, up to max_depth.""" + + def go(tri, depth): + if depth >= max_depth: + return [tri] + centre = np.mean(tri, axis=0) + if density(centre[0], centre[1]) < threshold: + return [tri] + return [out for part in split4(tri) for out in go(part, depth + 1)] + + return [out for tri in tris for out in go(tri, 0)] + + +def sampler(field): + """Looks a numpy field up at unit coordinates, for use as a density.""" + n = field.shape[0] + return lambda x, y: float(field[int(np.clip(y * n, 0, n - 1)), int(np.clip(x * n, 0, n - 1))]) + + +def feature_density(size=192): + """High where the pattern bends, which is where feature-adaptive refinement spends triangles. + + One large copy of the motif rather than a field of them: the point is that the triangles gather + somewhere and not elsewhere, and a pattern that bends everywhere has no elsewhere to show. + """ + m = motif(1, size) + gy, gx = np.gradient(m) + g = blur(np.hypot(gx, gy), 2) + return g / max(g.max(), 1e-6) + + +def irregular_mesh(seed=3): + """The uneven mesh a model arrives with: coarse here, fine there, nothing of one size.""" + rng = np.random.default_rng(seed) + tris = tri_mesh(4, jitter=0.9, seed=seed) + out = [] + for t in tris: + out.extend(split4(t) if rng.random() < 0.45 else [t]) + final = [] + for t in out: + final.extend(split4(t) if rng.random() < 0.3 else [t]) + return final + + +def snap_to_crease(tris, crease, distance=0.07): + """Pulls every vertex near the crease onto it, so mesh edges run along it instead of across.""" + (x0, y0), (x1, y1) = crease + d = np.array([x1 - x0, y1 - y0], float) + d /= np.linalg.norm(d) + + def snap(p): + t = np.dot(p - np.array([x0, y0]), d) + foot = np.array([x0, y0]) + t * d + return foot if np.linalg.norm(p - foot) < distance else p + + return [tuple(snap(np.asarray(v, float)) for v in t) for t in tris] + + +def sliver_mesh(seed=5): + """A mesh carrying the thin triangles refinement inherits from a model's own triangulation.""" + rng = np.random.default_rng(seed) + tris = tri_mesh(7, jitter=0.4, seed=seed) + out = [] + for t in tris: + if rng.random() < 0.45: + a, b, c = (np.asarray(v, float) for v in t) + mid = (b + c) * 0.5 + a = mid + (a - mid) * 0.08 # all but flattened onto the opposite edge + out.append((a, b, c)) + else: + out.append(t) + return out + + +def draw_profile(draw, size, height_1d, baseline=0.62, amplitude=0.30): + """A side view of a surface, as a filled region under its profile.""" + n = len(height_1d) + top = [(i / (n - 1.0) * size, (baseline - (height_1d[i] - 0.5) * amplitude) * size) for i in range(n)] + draw.polygon(top + [(size, size * 1.1), (0, size * 1.1)], fill=_rgb255((0.44, 0.46, 0.47))) + draw.line(top, fill=_rgb255((0.82, 0.88, 0.87)), width=max(1, int(1.8 * SS))) + + +def bayer(size, n=4): + """An ordered dither matrix tiled over the picture, for the mixing illustrations.""" + m = np.array([[0]]) + while m.shape[0] < n: + m = np.block([[4 * m, 4 * m + 2], [4 * m + 3, 4 * m + 1]]) + m = (m + 0.5) / m.size + return np.tile(m, (size // m.shape[0] + 1, size // m.shape[1] + 1))[:size, :size] + + +def levels_image(levels, dither=False, speckle=0.0, size=SIZE, seed=11): + """A smooth ramp reduced to a handful of greys - what a bake does to a photograph's colours. + + Flat rather than lit: this family of settings is about which filament a pixel ends up as, and a + relief would only add a second thing changing between two pictures that are meant to differ in + one. + """ + x, y = grid(size) + ramp = np.clip(0.5 + 0.5 * np.cos(2.0 * np.pi * (x + 0.25 * y)), 0.0, 1.0) + noise = bayer(size) - 0.5 if dither else 0.0 + q = np.round(np.clip(ramp + noise / max(levels - 1, 1), 0.0, 1.0) * (levels - 1)) / (levels - 1) + if speckle > 0.0: + rng = np.random.default_rng(seed) + hit = rng.random((size, size)) < speckle + q = np.where(hit, rng.integers(0, levels, (size, size)) / (levels - 1), q) + grey = 0.16 + 0.62 * q + return (np.dstack([grey, grey * 1.03, grey * 1.02]).clip(0.0, 1.0) * 255.0 + 0.5).astype(np.uint8) + + +def draw_mesh_lines(draw, size, tris, colour=(0.90, 0.94, 0.94), width=0.8): + """Only the edges, for drawing a mesh over a picture of the surface it sits on.""" + for t in tris: + pts = [_px(p, size) for p in t] + draw.line(pts + [pts[0]], fill=_rgb255(colour), width=max(1, int(width * SS))) + + +def draw_line(draw, size, segment, colour=(0.97, 0.99, 0.99)): + a, b = _px(segment[0], size), _px(segment[1], size) + draw.line([a, b], fill=_rgb255((0.08, 0.09, 0.10)), width=max(1, int(4.0 * SS))) + draw.line([a, b], fill=_rgb255(colour), width=max(1, int(1.4 * SS))) + + +def posterize(rgb, levels): + """The shaded picture reduced to flat bands - what the fast preview's per-fragment quantizing + does to a surface that the real preview shades smoothly.""" + v = rgb.astype(float) / 255.0 + return (np.round(v * (levels - 1)) / (levels - 1) * 255.0 + 0.5).astype(np.uint8) + + +def with_checker(rgb, squares=10, strength=0.30, warp=False): + """A checker laid over the surface, evenly or stretched. + + Even squares are what the Checker view is for - a texture laid out without stretching. The warped + version is the same squares through a mapping that does stretch, which is what the Distortion view + is there to find: cells crowded in one place and pulled apart in another. + """ + size = rgb.shape[0] + x, y = grid(size) + u, v = (np.sign(x) * np.abs(2.0 * x) ** 2.4 * 0.5, y * (1.0 + 0.9 * np.abs(x))) if warp else (x, y) + cells = (np.floor((u + 0.5) * squares) + np.floor((v + 0.5) * squares)) % 2.0 + return (rgb * (1.0 - strength * cells)[..., None]).astype(np.uint8) + + +def projected_sphere(kind, size=SIZE, squares=9): + """A ball with a checker laid on it by one of the panel's projections. + + One shape, five mappings: what differs between the pictures is only where the squares go, which is + the whole of what choosing a projection does. A sphere is the shape that shows it - every one of + these mappings is well behaved on a flat face and tells itself apart on a curved one. + """ + a = (np.arange(size) + 0.5) / size * 2.24 - 1.12 + x, y = np.meshgrid(a, a) + inside = x * x + y * y <= 1.0 + z = np.sqrt(np.clip(1.0 - x * x - y * y, 0.0, 1.0)) + + lam = np.clip(x * LIGHT[0] + y * LIGHT[1] + z * LIGHT[2], 0.0, 1.0) + rgb = SHADOW + (HIGHLIGHT - SHADOW) * (lam ** 0.85)[..., None] + + around = np.arctan2(x, z) / (2.0 * np.pi) + 0.5 + if kind == "sphere": + u, v = around, np.arccos(np.clip(-y, -1.0, 1.0)) / np.pi + elif kind == "cylinder": + u, v = around, (y + 1.0) * 0.5 + elif kind == "view": + u, v = (x + 1.0) * 0.5, (y + 1.0) * 0.5 + elif kind == "cube": + axis = np.argmax(np.abs(np.dstack([x, y, z])), axis=2) + u = np.where(axis == 0, z, x) + v = np.where(axis == 1, z, y) + u, v = (u + 1.0) * 0.5, (v + 1.0) * 0.5 + else: + raise ValueError(kind) + + cells = (np.floor(u * squares * 2.0) + np.floor(v * squares)) % 2.0 + rgb = rgb * (1.0 - 0.34 * cells)[..., None] + rgb = np.where(inside[..., None], rgb, np.array([0.13, 0.14, 0.15])) + return (np.clip(rgb, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8) + + +def uv_islands(size=SIZE, squares=9, seed=12): + """The flat layout an unwrap produces: the surface cut into islands and laid out side by side. + + Not a projection onto the model like the other four, so not drawn on one - an unwrap is the model + carrying its own coordinates, and this is what those look like. + """ + rng = np.random.default_rng(seed) + x, y = grid(size) + cells = (np.floor((x + 0.5) * squares * 2.0) + np.floor((y + 0.5) * squares)) % 2.0 + checker = (np.array([0.40, 0.43, 0.43]) + np.array([0.22, 0.24, 0.24]) * (1.0 - cells)[..., None]) + + def blob(centre, radius, lobes): + angles = np.linspace(0.0, 2.0 * np.pi, 26, endpoint=False) + wobble = 1.0 + 0.22 * np.sin(lobes * angles + rng.random() * 6.0) + return [(centre[0] + np.cos(t) * radius * w, centre[1] + np.sin(t) * radius * w) + for t, w in zip(angles, wobble)] + + islands = [blob((0.31, 0.32), 0.21, 3), blob((0.72, 0.30), 0.17, 4), + blob((0.34, 0.74), 0.17, 4), blob((0.72, 0.71), 0.20, 3)] + + mask = np.asarray(raster(lambda d, px: [d.polygon([(p[0] * px, p[1] * px) for p in island], fill=(255, 255, 255)) + for island in islands], + size, background=(0.0, 0.0, 0.0))).astype(float)[..., 0] / 255.0 + rgb = checker * mask[..., None] + np.array([0.13, 0.14, 0.15]) * (1.0 - mask)[..., None] + out = (np.clip(rgb, 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8) + return raster(lambda d, px: [d.polygon([(p[0] * px, p[1] * px) for p in island], outline=_rgb255(INK), + width=max(1, int(1.1 * SS))) + for island in islands], + size, background=out) + + def main(): os.makedirs(OUT_DIR, exist_ok=True) @@ -288,6 +686,108 @@ def main(): frames = [profile_view(wave, t, size=GIF_SIZE) for t in sweep] save_gif("midlevel.gif", frames, delay_ms=100) + # The paint tools, each shown doing the one thing that tells it apart from the others. + save_gif("paint.gif", brush_frames(GIF_SIZE, erase=False), delay_ms=110) + save_gif("erase.gif", brush_frames(GIF_SIZE, erase=True), delay_ms=110) + save_gif("tool_face.gif", face_frames(GIF_SIZE), delay_ms=180) + save_gif("tool_smart_fill.gif", flood_frames(GIF_SIZE, creases=[0.63]), delay_ms=110) + + # Brush size: the ring, and what it covers, over one size and back. + brush_tris = tri_mesh(9, jitter=0.55, seed=2) + brush_cents = centroids(brush_tris) + frames = [] + for i in range(steps): + r = 0.07 + 0.16 * (0.5 - 0.5 * np.cos(2.0 * np.pi * i / steps)) + under = np.linalg.norm(brush_cents - np.array([0.5, 0.5]), axis=1) < r + frames.append(raster(lambda d, sz, u=under, rr=r: (draw_mesh(d, sz, brush_tris, u), + draw_brush(d, sz, (0.5, 0.5), rr)), GIF_SIZE)) + save_gif("cursor_size.gif", frames, delay_ms=90) + + # Angle threshold: the same fill, let past one fold and then past both. + for name, creases in (("smart_fill_angle_low.png", [0.40, 0.70]), ("smart_fill_angle_high.png", [0.70])): + done = flood_frames(SIZE, creases=creases, steps=22, hold=0)[-1] + save_png(name, done) + + # Whole model: every face painted, and every face cleared. + whole = tri_mesh(9, jitter=0.55, seed=2) + save_png("select_all.png", raster(lambda d, sz: draw_mesh(d, sz, whole, np.ones(len(whole), bool)), SIZE)) + save_png("erase_all.png", raster(lambda d, sz: draw_mesh(d, sz, whole, np.zeros(len(whole), bool)), SIZE)) + + # The views the panel offers over the painted area. + relief = shade(motif(3)) + save_png("view_fast.png", posterize(relief, 3)) + # Checker and Distortion are about how the texture is laid out, not about the texture, so they go + # on a plain swell where the checker itself is what there is to look at. + plain = shade(0.25 + 0.5 * dome()) + save_png("view_checker.png", with_checker(plain, squares=7, strength=0.40)) + save_png("view_distortion.png", with_checker(plain, squares=7, strength=0.40, warp=True)) + save_png("view_wireframe.png", raster(lambda d, sz: draw_mesh_lines(d, sz, tri_mesh(9, jitter=0.55, seed=2)), + SIZE, background=relief)) + + # The projections: one ball, the checker put on it five different ways. + for kind in ("cube", "cylinder", "sphere", "view"): + save_png("map_%s.png" % kind, projected_sphere(kind)) + save_png("map_unwrap.png", uv_islands()) + + # Refinement: where the triangles go, and how many. + density_feature = sampler(feature_density()) + coarse = tri_mesh(3) + save_png("adaptive_off.png", raster(lambda d, sz: draw_mesh(d, sz, refine(coarse, lambda x, y: 1.0, 2)), SIZE)) + save_png("adaptive_on.png", raster(lambda d, sz: draw_mesh(d, sz, refine(coarse, density_feature, 3)), SIZE)) + + disc = sampler(painted_area(192, radius=0.34)) + painted_mesh = refine(coarse, disc, 3) + save_png("painted_only_on.png", + raster(lambda d, sz: draw_mesh(d, sz, painted_mesh, + np.array([disc(*np.mean(t, axis=0)) > 0.5 for t in painted_mesh])), SIZE)) + everywhere = refine(coarse, lambda x, y: 1.0, 3) + save_png("painted_only_off.png", + raster(lambda d, sz: draw_mesh(d, sz, everywhere, + np.array([disc(*np.mean(t, axis=0)) > 0.5 for t in everywhere])), SIZE)) + + # Subdivide: every triangle into four, a step at a time. + level = tri_mesh(2) + frames = [] + for _ in range(4): + frames.append(raster(lambda d, sz, m=level: draw_mesh(d, sz, m), GIF_SIZE)) + level = [out for t in level for out in split4(t)] + save_gif("subdivide.gif", frames, delay_ms=650) + + # Remesh: the model's own uneven triangulation, rebuilt at one size. + save_png("remesh_before.png", raster(lambda d, sz: draw_mesh(d, sz, irregular_mesh()), SIZE)) + save_png("remesh_after.png", raster(lambda d, sz: draw_mesh(d, sz, tri_mesh(8, jitter=0.35, seed=9)), SIZE)) + + # Hard edges: a fold the remesh runs across, against one it lays its edges along. + crease = ((0.06, 0.84), (0.94, 0.20)) + across = tri_mesh(5, jitter=0.45, seed=6) + save_png("preserve_edges_off.png", + raster(lambda d, sz: (draw_mesh(d, sz, across), draw_line(d, sz, crease)), SIZE)) + save_png("preserve_edges_on.png", + raster(lambda d, sz: (draw_mesh(d, sz, snap_to_crease(across, crease, 0.13)), + draw_line(d, sz, crease)), SIZE)) + + # Thin triangles, and the same mesh without them. + save_png("collapse_slivers_off.png", raster(lambda d, sz: draw_mesh(d, sz, sliver_mesh()), SIZE)) + save_png("collapse_slivers_on.png", raster(lambda d, sz: draw_mesh(d, sz, tri_mesh(7, jitter=0.4, seed=5)), SIZE)) + + # Smoothing the geometry after the texture is in it: the steps relax out of the surface. + stepped_profile = np.floor(profile_wave(GIF_SIZE, periods=2.0) * 5.0) / 4.0 + frames = [] + for t in sweep: + h = stepped_profile.copy() + for _ in range(int(round(t * 6))): + h = blur(h[None, :], 2)[0] + frames.append(raster(lambda d, sz, hh=h: draw_profile(d, sz, hh), GIF_SIZE, background=(0.11, 0.12, 0.13))) + save_gif("geometry_smoothing.gif", frames, delay_ms=100) + + # Mixing, how many mixes, and denoise - all about which filament a spot ends up as, so all flat. + save_png("mix_off.png", levels_image(4)) + save_png("mix_on.png", levels_image(4, dither=True)) + save_png("mixed_colors_few.png", levels_image(3, dither=True)) + save_png("mixed_colors_many.png", levels_image(8, dither=True)) + save_png("denoise_off.png", levels_image(5, dither=True, speckle=0.05)) + save_png("denoise_on.png", levels_image(5, dither=True)) + if __name__ == "__main__": main() diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp index 0a976f5dd7..f580abfeb3 100644 --- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp +++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp @@ -3791,10 +3791,18 @@ void GLGizmoTextureDisplacement::render_tip_media(const std::vector &m 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. - if (texture->is_animated()) + // The canvas only redraws on demand, so an animation has to ask for its own frames - and for + // exactly when the next one is due, rather than for a redraw as fast as the machine will go. + // + // Both halves are needed. schedule_extra_frame() alone only guarantees an idle event at that + // moment; GLCanvas3D::on_idle() returns without rendering anything unless something has marked + // the canvas dirty by then, which is why an animation left to that alone advanced one frame per + // mouse movement and stood still whenever the mouse did. set_dirty() is what on_idle reads + // (through GLGizmoBase::update_items_state()) to find out this gizmo still has frames to draw. + if (texture->is_animated()) { + set_dirty(); m_parent.schedule_extra_frame(int(next * 1000.0)); + } if (!entry.caption.empty()) { // Centred under its own illustration, and never wider than its column. @@ -5962,8 +5970,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(), - const std::vector &media = {}) -> bool { + const wxString &tip_text, const std::vector &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)); @@ -6215,11 +6223,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; } @@ -6233,20 +6243,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; } @@ -6260,11 +6272,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; @@ -6287,7 +6301,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float centre_on_row(frame_h); ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", ImGuiSliderFlags_AlwaysClamp); - hover_tip(m_desc.at("cursor_size")); + hover_tip(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); @@ -6304,7 +6318,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float centre_on_row(frame_h); ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", ImGuiSliderFlags_AlwaysClamp); - hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this")); + hover_tip(_u8L("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(); @@ -6341,19 +6356,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 = 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 = 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 = 2; open_uv_editor = true; } @@ -6361,13 +6378,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 = 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"); @@ -6671,7 +6689,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 @@ -6680,7 +6699,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad)) m_preview_params_dirty = true; hover_tip(_u8L("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") } }); // After the slider, so a change shows in the same frame. ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"), int(cached_palette().size()), int(m_palette_filaments.size())).c_str()); @@ -6689,7 +6709,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float m_preview_params_dirty = true; hover_tip(_u8L("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") } }); } } @@ -6705,29 +6726,35 @@ 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 }; 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" }, }; for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) { if (mi > 0) ImGui::SameLine(0.f, gap_s); const auto method = static_cast(mi); if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label, - mappings[mi].tip) && + mappings[mi].tip, { { mappings[mi].picture } }) && 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. @@ -6837,14 +6864,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, { { "tile_repeat.png" } })) { + icon_sm, _L("Repeat"), _L("Repeat"), { { "tile_repeat.png" } }, tile_na)) { 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_mirror.png" } })) { + { { "tile_mirror.png" } }, tile_na)) { layer.tile_method = static_cast(1); m_preview_params_dirty = true; } @@ -7066,7 +7093,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) { @@ -7094,7 +7122,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 @@ -7151,7 +7180,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float } hover_tip(_u8L("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" } }); } { @@ -7215,12 +7245,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); hover_tip(_u8L("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); ImGui::SetNextItemWidth(-1.f); @@ -7236,7 +7268,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) { @@ -7252,7 +7285,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)) { @@ -7316,7 +7350,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" } }); } if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) { // Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines @@ -7331,7 +7366,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) @@ -7442,7 +7478,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float "smaller than the detail you want out of the texture - a 0.5 mm groove needs " "triangles well under 0.5 mm. 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 detail goes with it.")); + "back down and the detail goes with it."), + { { "subdivide.gif" } }); // What this resolution costs over what is painted, against what the budget allows. Refining // past the budget is not an error - the bake simplifies back down to it - but the result @@ -7485,7 +7522,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float m_imgui->disabled_end(); hover_tip(_u8L("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" } }); } const float button_h = std::round(frame_h * 1.25f);