Compare commits

..

31 Commits

Author SHA1 Message Date
ExPikaPaka
013ac898ba Improve texture displacement smoothing and UV editor framing 2026-09-09 15:25:00 +02:00
ExPikaPaka
3aefae016f Add new bake pipeline 2026-09-03 09:13:14 +02:00
ExPikaPaka
96f2c8e698 Merge branch 'feature/texture_displacement' of https://github.com/OrcaSlicer/OrcaSlicer into feature/texture_displacement 2026-08-31 08:45:38 +02:00
ExPikaPaka
d855bab19d Add color suport for textures 2026-08-31 07:58:48 +02:00
SoftFever
cde889a7c3 Merge branch 'main' into feature/texture_displacement 2026-08-30 13:52:54 +08:00
ExPikaPaka
045504c880 Move computation to background thread & resolve freeze 2026-08-27 09:20:02 +02:00
ExPikaPaka
6166bac17a Improve adaptive subdivision at border & fix some visual bugs 2026-08-26 09:13:50 +02:00
ExPikaPaka
165a1e9f4c Measure post-process smoothing by edge energy instead of height spread 2026-08-20 07:14:21 +02:00
ExPikaPaka
c215d0291b Merge branch 'main' into feature/texture_displacement 2026-08-19 09:13:53 +02:00
ExPikaPaka
d926f7c490 Add Parallax preview & fix Undo\Redo history 2026-08-12 10:14:38 +02:00
ExPikaPaka
2db770b672 Displace the painted patch border and add post-process smoothing 2026-07-29 08:44:16 +02:00
ExPikaPaka
2a46197322 Rewrite adaptive subdivision to refine worst-first against a triangle budget 2026-07-28 11:42:40 +02:00
ExPikaPaka
15fd3fc97c Add adaptive subdivision 2026-07-24 09:03:06 +02:00
ExPikaPaka
00f778e863 Merge branch 'feature/texture_displacement' of https://github.com/OrcaSlicer/OrcaSlicer into feature/texture_displacement 2026-07-21 14:24:39 +02:00
ExPikaPaka
dc1e8b6dc3 Rename reserved GLSL word on AMD GPU 2026-07-21 14:07:51 +02:00
SoftFever
753054974b Merge branch 'main' into feature/texture_displacement 2026-07-21 19:50:35 +08:00
ExPikaPaka
eacc236ccb Fix typo again 2026-07-21 12:53:00 +02:00
ExPikaPaka
345369e005 Fix typo after cleanup 2026-07-21 11:28:51 +02:00
ExPikaPaka
18c5d31fb2 Merge branch 'feature/texture_displacement' of https://github.com/OrcaSlicer/OrcaSlicer into feature/texture_displacement 2026-07-21 10:12:16 +02:00
ExPikaPaka
61d2d4355a Cleanup 2026-07-21 10:12:09 +02:00
ExPikaPaka
ab023f3f6d Add texture projection frame overlay, fix remeshing and subdivision 2026-07-21 09:44:25 +02:00
Ian Bassi
b073aa4d23 Merge branch 'main' into feature/texture_displacement 2026-07-17 09:07:31 -03:00
ExPikaPaka
8247514ae2 Fix cmake config 2026-07-17 09:04:39 +02:00
ExPikaPaka
00f55639d3 Add new icons 2026-07-16 08:52:15 +02:00
ExPikaPaka
68d754d946 Add texture displacement documentation 2026-07-16 08:43:35 +02:00
ExPikaPaka
dc5a48bdfd Add texture displacement toolbar icon and textures 2026-07-16 08:43:27 +02:00
ExPikaPaka
05083bb6ab Add texture displacement gizmo and UV editor 2026-07-16 08:43:21 +02:00
ExPikaPaka
a393b21642 Add texture displacement bump and UV-check shaders 2026-07-16 08:43:08 +02:00
ExPikaPaka
a7c8dcc58d Add texture displacement baking, LSCM unwrap and remesh core 2026-07-16 08:42:55 +02:00
ExPikaPaka
3514249197 Removed files that were accidently added 2026-07-09 08:40:37 +02:00
ExPikaPaka
7f2598d0d6 POC 2026-07-08 08:50:47 +02:00
152 changed files with 23905 additions and 1416 deletions

View File

@@ -151,7 +151,7 @@ jobs:
if [ -z "${{ vars.SELF_HOSTED }}" ]; then
brew install automake texinfo libtool pkgconf yasm nasm
fi
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15
./build_release_macos.sh -dx ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15
(cd "${{ github.workspace }}/deps/build/${{ inputs.arch }}" && \
find . -mindepth 1 -maxdepth 1 ! -name 'OrcaSlicer_dep' -exec rm -rf {} +)

View File

@@ -145,7 +145,7 @@ jobs:
env:
ORCA_TESTS_BUILD_ONLY: ${{ inputs.arch == 'arm64' && '1' || '' }}
run: |
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
./build_release_macos.sh -s -n -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a ${{ inputs.arch }} -t 10.15 ${{ inputs.arch == 'arm64' && '-T' || '' }}
- name: Pack unit tests mac
if: runner.os == 'macOS' && !inputs.macos-combine-only && inputs.arch == 'arm64'
@@ -204,7 +204,7 @@ jobs:
if: runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-j 3' || '' }} -a universal -t 10.15
./build_release_macos.sh -u -x ${{ !vars.SELF_HOSTED && '-1' || '' }} -a universal -t 10.15
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
@@ -385,6 +385,13 @@ jobs:
dir "C:/Program Files (x86)/Windows Kits/10/Include"
choco install nsis
- name: Install pkg-config
# FFmpeg is discovered via pkg-config (pkg_check_modules LIBAV in
# src/slic3r/CMakeLists.txt); the Windows runners don't ship it.
if: runner.os == 'Windows' && !vars.SELF_HOSTED
run: |
choco install pkgconfiglite -y
- name: Build slicer Win
if: runner.os == 'Windows'
working-directory: ${{ github.workspace }}

View File

@@ -476,7 +476,8 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
# if(WIN32)
if(WIN32)
find_package(PkgConfig REQUIRED)
# if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
@@ -512,7 +513,7 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# else()
# message("Building without Win10 Netfabb STL fixing service support")
# endif()
# endif()
endif()
if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")

581
TEXTURE_DISPLACEMENT.md Normal file
View File

@@ -0,0 +1,581 @@
# Texture Displacement - Technical Notes
Branch: `feature/texture_displacement`. Reference for the feature as it stands: what it does, how the
algorithms work, and where the code lives.
## What it does
A paint-style gizmo (`GLGizmoTextureDisplacement`) that lets you:
- Paint one or more "layers" onto a model's surface, each a height-map texture with its own
depth/tiling/rotation/offset/invert/tile-mode/projection-mode/blend-mode.
- Pick a texture from a shipped library (`resources/textures/displacement/`) or import your own
(saved into `<data_dir>/textures/displacement/`, kept separate so app updates can't clobber it).
- Combine overlapping layers with image-editor-style blend modes (Add/Subtract/Multiply/Divide).
- Preview the true displaced result live, before baking (background job, not on the UI thread).
- Preview via a fast GPU shader instead (no real geometry movement) for a lighter-weight alternative.
- Bake into real mesh geometry on demand, restricted to the painted area only.
- Remesh and subdivide so a low-poly model has enough vertices to show fine detail.
- Unwrap a painted patch with a real CGAL LSCM parameterization and view it in a dedicated,
dockable 2D "UV Editor" pane.
## Standard vs Pro mode
A two-position slider in the panel header, right of the Dock/Undock button.
**Pro** shows every mesh-preparation control; Remesh, Subdivide and Bake are run separately by the user,
in whatever order they like.
**Standard** hides all of it and folds one fixed recipe into the Bake button, because a height map only
ever *moves vertices that already exist* - painting onto an imported 12-triangle box and pressing Bake
would otherwise do nothing visible. Standard's Bake is:
1. `plan_remesh()` + `replace_mesh_keep_all_paint()` - isotropic remesh to 1 mm, sharp edges above 40
degrees protected. Gives the subdivider an even starting density whatever the input looked like.
2. `plan_adaptive_subdivision()` + `apply_adaptive_subdivision()` - feature-adaptive refinement, max
edge 20 mm, detail 0.02 mm, min edge 0.02 mm.
3. `bake()` - the ordinary background displacement job.
Both preparation stages are *planned* before the undo snapshot and *applied* after it, so a stage with
nothing to do is skipped without leaving an empty undo step. The standalone Pro buttons share the same
plan/apply split.
**All three stages sit under one undo step.** `Plater::take_snapshot()` records the state *before* the
change, so a single snapshot taken at the top of `bake_standard()` means one Undo returns the mesh to
exactly what was imported. `TextureDisplacementBakeInput::take_snapshot` lets the caller say who owns
the undo step - true for the Pro-mode button, false for the pipeline, whose background job commits long
after that snapshot's scope has closed.
The presets live in one place (`STD_*` constants) and `apply_standard_mode_presets()` pins the hidden
controls to them every frame while Standard is active, so the live preview cannot disagree with what
Bake will do. Switching to Standard also closes the subdivision preview, whose controls have just gone.
One control survives into Standard: **"Added triangles (k)"**, the subdivision budget. It is deliberately
*not* pinned - pinning would fight the user's own slider every frame - because unlike the rest of the
recipe its right value depends on the part rather than on the method (a big model, or a fine texture,
simply needs more triangles). Default 1500. The widget is one lambda shared by both layouts.
Standard remeshes *after* painting, so the remesh has to preserve paint: `ModelVolume::restore_painting()`
only remaps the four standard channels, so `replace_mesh_keep_all_paint()` additionally runs
`TriangleSelector::remap_painting()` over the eight texture-displacement masks. The Pro Remesh button
goes through the same helper. If the remap comes back empty the pipeline stops with a message rather
than baking a flat mesh.
## Architecture
### Data model (per `ModelVolume`)
Each of up to `TEXTURE_DISPLACEMENT_MAX_LAYERS` (8) layers gets its **own independent
`FacetsAnnotation`** paint mask - the same `TriangleSelector`/`FacetsAnnotation` machinery every other
paint gizmo (FdmSupports, Seam, MMU, FuzzySkin) already uses, just one full instance per layer slot
instead of one per volume. This is what makes layered/blended painting work for free: the same triangle
can be `ENFORCER` in layer 2's mask and layer 5's mask simultaneously, and at bake/preview time each
layer displaces the surface left by the previous one (image-editor-layer semantics).
Whole-stack settings (border handling, post-process smoothing) live beside the layers in
`texture_displacement_options` (`TextureDisplacementOptions`), since they belong to no single layer.
### Bake algorithm (`libslic3r/TextureDisplacement.cpp`)
`build_texture_displacement(base_mesh, layers, facets_data, options)` is **accumulate-then-displace,
and topology-preserving**: the returned mesh has exactly the input's vertices and triangles, in the
same order - only the positions of displaced vertices differ.
1. `its_compactify_vertices()` on a copy of the input. In practice a no-op (it only drops
*unreferenced* vertices, and preserves the order and indices of the rest). It is there to
guarantee the index alignment step 3 depends on.
2. Area-weighted vertex normals of the **undisplaced** mesh, computed once. Every layer both projects
and displaces along these, so a vertex covered by several layers moves along one single well-defined
direction. Where the paint does *not* cover every triangle around a vertex, the normal is recomputed
from the painted triangles alone (the union over all layers, so it stays one direction per vertex):
on the rim of a fully painted top face the whole-mesh normal is the 45-degree bisector it shares with
the side wall, and displacing along that flares the rim outwards instead of raising it. Interior
vertices are unaffected - all their triangles are painted, so the two normals coincide. Paint
coverage per original triangle comes straight off `TriangleSplittingData::triangles_to_split`.
3. For each layer in slot order: deserialize its stored paint mask into a `TriangleSelector` against
the **base mesh** (never against a previous layer's output), then
`selector.get_facets_strict(ENFORCER)` → the painted patch. Two facts are exploited:
- `get_facets_strict()` returns the mesh's **entire** referenced vertex array regardless of which
state was asked for - only `.indices` is filtered by state. So `get_facets_strict(ENFORCER)`
and `get_facets_strict(NONE)` share identical vertex indexing, which is what lets boundary
detection be a plain index check instead of a position-hash lookup.
- The selector's vertex array *starts with* the mesh's own vertices (extra ones created where a
brush stroke split a triangle are appended after them), and `get_facets_strict()` emits the
referenced ones in order. Combined with step 1, **selector vertex index `i` is our vertex `i`**.
Split vertices live past the end of our array and are simply skipped - they sit on the paint
boundary anyway (splitting only happens at partial coverage).
4. A vertex used by at least one **unpainted** triangle is a border vertex. Whether it moves is
`TextureDisplacementOptions::displace_border`, and it does by default. Nothing can tear: the bake is
topology-preserving, so a border vertex is *one* vertex shared by both regions and moving it simply
tilts the unpainted triangles that use it. Pinning it instead clamps the outermost ring of relief to
zero, which on a fully painted face collapses the pattern into a ring of steep ramps at the edge; it
is kept as an option for when the relief must not spill past the paint at all. Either way the border
drives the `edge_smoothing` falloff.
5. Per interior vertex: sample the height texture (`sample_layer_height()`, see Projection methods)
and fold `height * depth_mm * (invert ? -1 : 1)` into that vertex's running total via the layer's
`TextureBlendMode` (see Blend modes). A `visited` set makes each layer fold in exactly **once**
per vertex, no matter how many of the patch's triangles share it - otherwise a Multiply/Subtract
layer would apply two or three times over depending on local triangle fan-out.
6. Move each touched vertex along its (step 2) normal by its accumulated total.
7. Optionally (`TextureDisplacementOptions::smooth_*`) relax the result - see Post-process smoothing.
### Post-process smoothing
`smooth_mesh_vertices(mesh, movable, strength, iterations)` - Laplacian relaxation, run after all layers
have been folded in, restricted to the vertices flagged in `movable`. Each pass moves a movable vertex a
`strength` fraction of the way to the average of its one-ring, read from a **snapshot** of the previous
pass so the result does not depend on vertex order (a Gauss-Seidel sweep would smooth several times as
hard at the end of the array as at the start). Neighbours come from a CSR-style adjacency built once per
call. Topology-preserving, like the bake.
Its job is to round off the hard steps a bitmap height map leaves behind - a different knob from
`TextureDisplacementLayer::smoothing`, which blurs the *height map* before it is ever sampled.
Two ways in, sharing one set of settings on the volume:
- The **"Smooth result"** checkbox + "Smoothing (%)" / "Passes" ride along with Preview and Bake.
`movable` is exactly the set of vertices the displacement moved, so the untouched part of the model
keeps its exact geometry and the ring just outside the displaced set anchors the relaxation (the
relief cannot creep outward).
- **"Smooth baked mesh now"** (`GLGizmoTextureDisplacement::smooth_model()`) applies the same settings to
the volume's *committed* geometry, for relief that is already baked in. `movable` there is the painted
triangles' vertices. Because smoothing never touches the triangle list, this is the one geometry
operation in the gizmo that keeps **every** paint channel verbatim - it saves and restores the eight
texture-displacement masks around `set_mesh()` rather than remapping or dropping them.
**"Ignore outer ring"** (`smooth_skip_border`, on by default) drops the patch's own outermost ring of
vertices from `movable`. That ring's neighbours *outside* the paint never move, so relaxing it drags the
rim of the relief down toward the flat surface and the pattern comes out half-melted where it meets the
edge. Held out, the border keeps the full depth the texture asked for and only the interior relaxes.
Turning it off softens the outer edge deliberately (a blunter version of the per-layer edge-smoothing
falloff). This is the *smoothing* rim, independent of whether that rim is displaced at all
(`displace_border`, step 4 above); both default to keeping the border sharp.
### Blend modes
`TextureBlendMode` {Add, Subtract, Multiply, Divide}, per layer, applied per vertex against the
total accumulated by the layers **below** it (lower slots). The quantity blended is a signed
displacement in **mm**, not a pixel value.
Add/Subtract are self-explanatory. Multiply/Divide are *scaling* operations and so need a unit
convention: they treat the layer's own value as a **factor relative to 1 mm**. That makes `depth_mm`
a gain, and - the property that makes a Multiply layer usable as a mask - a layer with depth 1 mm
sampling a white (1.0) texel multiplies by exactly 1, i.e. leaves the layers below unchanged.
Divide floors its divisor's magnitude at 0.05: a black texel samples to *exactly* zero, so the divisor
really does hit zero in ordinary use, and an unbounded `1/0` would fling vertices thousands of mm away
and poison the mesh's bounding box (and every plate/print-volume check downstream). The floor doubles as
a cap on how far Divide can amplify the relief beneath it: at most 20×.
The **lowest painted layer ignores its blend mode**: it has nothing beneath it, and Multiply/Divide
against an implicit zero base would annihilate (or blow up) it. Enforced in
`build_texture_displacement()` (the first layer to reach a given vertex always folds in additively) and
surfaced in the UI, which labels that layer "Base layer" instead of offering a control that does nothing.
### Projection methods
Five choices per layer (`TextureProjectionMethod`), all funneling through `apply_uv_transform()`
(scale by `1/tiling_scale`, rotate by `rotation_deg`, add `offset`). They are dispatched by
`sample_layer_height()`, which returns a **height**, not a UV - because Triplanar takes three
texture samples per vertex and so has no single UV that represents it.
- **Triplanar** (default) - samples the texture on all three world planes (`(y,z)`, `(x,z)`, `(x,y)`)
and blends the three by the vertex's own normal raised to `TRIPLANAR_BLEND_SHARPNESS` (4). Hard-picking
the single axis most aligned with the normal instead is discontinuous wherever that dominant axis
flips: on a +X face the planar coordinate is `(y, z)`, on a Y face it is `(x, z)`, so at the shared
edge `u` jumps. A weighted blend is continuous across the transition by construction, since the weight
of the axis being left behind falls smoothly to zero. This removes the hard *seam*; some cross-fade
blurring in the band right at a 90° edge is inherent to triplanar mapping. A genuinely seam-free wrap
around a box needs a real unwrap - that is what the LSCM mode is for.
- **Cylindrical** - wraps around an axis through the patch centroid, axis auto-picked as the world
axis *least* aligned with the average normal (perpendicular to the outward radial normal, as a
cylinder's own axis would be). `u = angle * local_radius` (arc length in mm), `v = distance along
axis`. An approximation, not an exact fit for arbitrary geometry, and the axis/centre are not
user-overridable.
- **Spherical** - longitude/latitude around the centroid, scaled by local radius. Same caveat.
- **LSCM** - real UV unwrap via `MeshBoolean::cgal::parameterize_lscm()` (CGAL's
`Surface_mesh_parameterization` package, LSCM algorithm). Computed **once per patch** (not
per-vertex like the others - it's a single global least-squares solve), then each vertex looks up
its precomputed UV. Requires the patch to be a single topological disk (one connected component,
one boundary loop) - `compute_lscm_uvs()` returns empty and the layer falls back to Triplanar if not
(e.g. multiple disconnected painted islands, or a fully closed patch). CGAL's parameterizer needs a
mesh with no isolated/unreferenced vertices, but `get_facets_strict()` returns the *whole* mesh's
vertex array - so `compact_patch_with_map()` builds a clean sub-mesh plus an index map back to the
original vertex numbering, purely local to this file.
- **ViewProjected** ("From view") - a flat projection along a fixed direction captured from the 3D
camera, like a slide projector. `capture_view_projection()` takes the camera's right/up axes,
transforms them into the volume's *local* frame (so the projection rides along if the part is later
moved), and stores them as `TextureDisplacementLayer::view_project_right/up` (unit vectors, so the
projected coordinate stays in mm and `tiling_scale` keeps meaning mm). `sample_layer_height()`
projects `Vec2f(dot(pos, right), dot(pos, up))`. Single-valued per point, so - like LSCM but unlike
blended Triplanar - the fast preview and UV-check overlay precompute it per vertex
(`compute_layer_vertex_uvs()`) and drive the shader's `use_vertex_uv` path. Faces angled away from
the projector smear; that is inherent to view projection.
Two companions to this mode:
- **Projection frame overlay** (`TextureProjectorFrame`, see below) - a semi-transparent window
dragged over the 3D view whose border becomes the projection's edge. Applying it stores an exact
**projective** map in `view_project_matrix`, which supersedes the affine `right`/`up` axes above
for that layer (`view_project_projective`).
- **"Project only on visible"** (`select_visible_faces()`) - repaints the layer with exactly the
facets the camera can see, so the projected area matches the viewpoint the projector was captured
from. Two tests: a facing test (normal vs. view direction, per triangle - under perspective the
view direction varies across the model, so it is taken from the eye to each centroid), then
`MeshRaycaster::get_unobscured_idxs()` on the survivors to drop facets hidden behind other
geometry, so a concave part's far inner wall is correctly excluded. One ray query per front-facing
facet, hence click-driven (on the checkbox and on each "Capture current view"), never per frame.
It **replaces** the layer's paint rather than adding to it - "project onto what I can see" would
otherwise accumulate every angle the user had ever looked from.
### Manual seams and island cutting
`TextureDisplacementLayer::lscm_seam_edges` - undirected mesh-vertex-index edge pairs the unwrap is
forced to cut along, on top of the dihedral-angle seams. `segment_into_charts()` takes a set of these
(translated from mesh → compacted-patch numbering inside `compute_patch_unwrap()`) and refuses to
union two triangles across a marked edge whatever their angle. Both the unwrap cache key and the
gizmo's `UVEditorState` include the seam list, so marking a seam (which leaves the paint mask
untouched) still forces a re-solve. Like the paint masks, seams are mesh-index-space and so dropped on
any topology change.
Two ways to write to it:
- **Mark seam (manual)** - a "Mark seams" click mode (`m_seam_edit_mode`) that suppresses painting. A
click raycasts the volume (`m_c->raycaster()->raycasters()[idx]->unproject_on_mesh()`, `idx` = the
volume's slot among model-part volumes), finds the facet's edge nearest the hit point, and toggles it.
Marked edges render as a red overlay (`render_seam_overlay()`), pulled toward the camera so they read
on top. This is the Blender mark-seam workflow.
- **Cut island (auto)** - `cut_island()` takes the selected chart's triangles (back-mapped from the
unwrap via `source_vertex`), finds their 3D bounding box, and marks every edge that straddles the
mid-plane perpendicular to the longest axis. The re-unwrap then splits the chart across its narrow
waist. Exposed as the UV pane's **Cut** button.
### UV-check overlays (checker / distortion)
`resources/shaders/{110,140}/texture_displacement_uvcheck.{vs,fs}`, one shader with a `mode` uniform,
drawn over the painted patch (`rebuild_uvcheck_mesh()`/`render_uvcheck_mesh()`, P3N3T2: `normal.x` =
distortion, `tex_coord` = uv), pulled forward with a polygon offset. **Checker** samples a procedural
checkerboard at the layer's uv (per-vertex for LSCM/ViewProjected, in-shader triplanar otherwise) -
squares that stay square mean low distortion. **Distortion** colours each triangle blue→green→red by
`log2(uv_area / surface_area)` centred on the patch's *median* stretch (so a globally-scaled unwrap
reads as uniformly ideal and only relative stretch shows), averaged to vertices. A separate **Show mesh
wireframe** toggle draws the whole volume's triangle edges, rebuilt only when the vertex count changes
(not per stroke).
### Tiling
`DecodedHeightTexture::sample(uv, tile_enabled, tile_method)`. Two tile methods when enabled
(Repeat, MirroredRepeat). **When `tile_enabled` is false, sampling outside `[0,1)` returns `0`
directly** rather than clamping the *coordinate* into range, which would smear the border row/column of
pixels outward to infinity in every direction (streaky lines radiating out from the painted patch).
### Subdivision — two modes
**Uniform (`subdivide_mesh_uniform()`)** — whole-mesh, 1-to-4 split. Recursive edge-midpoint split with
a shared per-pass midpoint cache (keyed by sorted vertex-index pair) so triangles sharing an edge get
the *same* new vertex - capped at `max_iterations` (default 6). Whole-mesh so it never leaves a
T-junction, at the cost of densifying everywhere. Wired as a "Subdivide steps" slider (**05**, 0 =
no subdivision), Apply snaps back to 0. Drops texture-displacement paint (no remap) via the standard
`save_painting()`/`set_mesh()`/`restore_painting()` dance; the other four channels are remapped.
**Adaptive (`subdivide_mesh_adaptive()`)** — refine **only the painted area**, by **Rivara longest-edge
bisection**, which is *conformal by construction*. Only **terminal** edges are ever bisected - an edge
that is the longest edge of *every* triangle sharing it - which splits both those triangles along one
shared midpoint at once, so a hanging node is never created. The edge to split for a triangle that wants
refining is found by **longest-edge propagation (LEPP)**: walk to the longest edge of ever-longer-edged
neighbours until a terminal one is reached, and bisect that. Edge length strictly increases along the
path (ties broken by mesh-vertex key, which both sides of an edge compute identically), so the walk
cannot cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
of bisections. The transition triangles it pulls in just outside the painted patch are the graded band
that makes the size change conformal.
The win: a small decal on a big model no longer quadruples the *whole* model's triangle count.
**Run to completion, worst-first, against a triangle budget.** The refinement loop is not a fixed number
of sweeps: it holds every triangle that is over its criteria in a max-heap keyed by *how many times over*
it is, pops the worst, walks its LEPP, bisects, and re-scores. Edge adjacency (`nb[e]`, the triangle
across each edge) is built **once** and maintained incrementally through each bisection, so the cost
scales with the refined region rather than with the whole model. `max_triangles` is the only bound;
stopping on it leaves a perfectly valid, still-conformal mesh that spent its budget on the largest
errors. A fixed sweep count instead spends itself grading the *coarse surroundings* - whose edges are
the longest, so they win every terminal-edge contest - and never reaches the painted patch.
**It carries the paint forward**, which is what makes it usable (uniform subdivide drops paint). Because
the refinement is *driven by* the paint, the remap is trivial: `subdivide_mesh_adaptive()` fills an
`out_source[new_tri] = input_tri` map (children inherit their parent), and the gizmo rebuilds each
layer's mask on the new mesh - a new triangle is painted iff its source was fully painted in that
layer. `collect_paint_region()` derives both:
- the union refine-region: **exactly** the original triangles the brush touched, read straight off
`TriangleSplittingData::triangles_to_split` (`serialize()` records an entry per original triangle that
is either split - i.e. partially painted, the patch boundary - or carries a non-default state). No
dilation: marking every triangle that shares a *vertex* with the patch drags in a whole fan of huge
unpainted neighbours and refines *those* down to the resolution floor, since the height field the
detail test samples is not restricted to the painted area. The conformal closure already grades the
size change outward on its own.
- the per-layer fully-painted-triangle sets (a `get_facets_strict(ENFORCER)` sub-triangle with all three
*original* vertex indices == a whole, fully-painted original triangle; a partial stroke's sub-triangles
always carry a split vertex).
The other four channels ride the normal `restore_painting()` remap.
Both modes share the gizmo's Preview/Apply/Done flow; the **"Only painted area (adaptive)"** checkbox
picks the mode, and the adaptive preview follows the paint live (`rebuild_preview()` refreshes the
wireframe while the subdivide preview is open in adaptive mode). The panel shows the previewed triangle
count.
**Feature-adaptive (follow texture detail).** A sub-mode of adaptive (the **"Follow texture detail"**
checkbox) that puts triangles where the *displaced surface actually bends*, not evenly. A flat region or
a linear **ramp** needs no extra vertices (linear interpolation is exact for a ramp); what needs them is
**curvature** - the *second* derivative, not the gradient. So the extra predicate is a **chord-error**
test: sample the combined displacement at the triangle's three edge midpoints *and its centroid*
(sampling the interior is what catches a bump sitting inside a triangle, the blind spot of an edge-only
test) and take the largest departure from the flat triangle's barycentric interpolation. Refine while
that exceeds `chord_tolerance_mm` ("Detail (mm)"). Zero chord error on a ramp ⇒ untouched; high on a
bump/ridge/noise ⇒ refined until captured. Same conformal machinery, so still crack-free. The
per-triangle error is cached and recomputed only for the children of a split.
Four knobs bracket it, and all four matter:
- **"Max edge (mm)"** (`target_edge_length_mm`) is a **baseline that applies in feature mode too**.
Without it the chord test aliases: a big triangle over a fine pattern can sample four points that all
land at similar heights, report no error, and stall before refinement ever starts. The baseline
guarantees a sampling density fine enough for the curvature test to see the texture at all.
- **"Detail (mm)"** is the chord tolerance above.
- **"Min edge (mm)"** is a hard floor under both, and is what guarantees termination across a sharp
texture *step*, where the error never falls however fine the mesh gets.
- **"Added triangles (k)"** is the budget, passed as `max_triangles` (the model's own triangle count plus
the slider, so the control still means something on an already-dense model).
The height field is `make_combined_displacement_sampler()` - it mirrors `build_texture_displacement()`'s
per-layer setup (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively")
but evaluated per point. Two deliberate simplifications, both erring toward *more* detail (safe -
over-refinement is never a crack): every sampleable layer is sampled at every point (no per-point paint
test), and edge-smoothing falloff is ignored. The first is *why* the refine region must not be dilated -
outside the paint the sampler still reports full relief. **LSCM layers are skipped** (no per-point UV); a
purely LSCM stack yields a null sampler and the code falls back to the length baseline alone. Per-vertex
heights are sampled lazily, so a small patch on a huge model never pays for the rest of it.
### Fast preview (GPU-only, no CPU meshing)
`resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}`, registered as
`"texture_displacement_bump"`. Shades the *displaced* surface without moving geometry - active-layer
only, selected from the View row, and the default when the gizmo opens (`m_use_bump_preview = true`).
Vertex format is `GLModel::Geometry::EVertexLayout::P3N3T2`: `normal.x` carries the per-vertex paint
weight (0/1), `normal.y` flags the UV island currently being dragged, and `tex_coord` carries a
precomputed texture UV, so it can use `GLModel` normally instead of a hand-rolled VBO/VAO manager.
The mesh is **flat** (vertices not shared between triangles): every corner of a painted triangle gets
weight 1, every corner of an unpainted one weight 0. A coarse mesh needs that - one painted face of a raw
cube has no strictly-interior vertex, so per-vertex weighting would either bleed onto the neighbours or
vanish outright. Duplicating vertices costs no shading quality here because the shader takes its surface
normal from screen-space derivatives of position, not from a per-vertex normal.
**Both preview meshes work in the patch's vertex space, not the mesh's.** Those agree only until a
*brush* stroke splits a triangle: `get_facets_strict()` then appends the split vertices, so the patch
array is longer. `rebuild_bump_preview_mesh()` and `rebuild_uvcheck_mesh()` therefore index
`patch.vertices` throughout. The weight buffer is rebuilt at the same cadence as the true-displacement
preview (stroke-end/slider-release) but from the **live** `TriangleSelector` state, not the flushed model
facets, so it does not lag by a full model round-trip.
The perturbed normal is the analytic one for a height field `H = ±depth_mm · h(uv)` displaced along
`N` over any orthonormal surface tangent pair `T`/`B`:
N' = normalize(N (dH/da)·T (dH/db)·B), a = dot(p,T), b = dot(p,B)
The two slopes have to be genuine **mm-per-mm** derivatives for the preview's apparent depth to match
the bake's.
**Two projection paths (`use_vertex_uv` uniform):**
- **Triplanar (`use_vertex_uv = 0`)** - `uv` and the `T`/`B` axes are both derived in-shader from
the dominant normal component, mirroring `project_planar()`/`apply_uv_transform()`, and the slope is
formed analytically. `T`/`B` are the projection's axis-aligned pair, exact only when the face is
axis-aligned; the shader drops the along-normal component to keep the gradient in the surface. Here
one `uv` unit is exactly `tiling_scale` mm, so the `1/tiling_scale` gradient factor is right.
- **Precomputed UV (`use_vertex_uv = 1`, used for LSCM and ViewProjected)** - `uv` comes per-vertex from
the CPU (`compute_layer_vertex_uvs()`, so island placement + tiling/rotation/offset are already folded
in), and the perturbed normal is built with **Mikkelsen's method** ("Bump Mapping Unparametrized
Surfaces on the GPU"): the surface gradient taken directly from the screen-space derivatives of the
*sampled height* and position. **This makes no uv→mm scale assumption**, which is essential, because an
LSCM map is **conformal, not isometric**: it is globally area-scaled but the *local* mm-per-uv varies
across the chart, so a single global `1/tiling_scale` factor gets the apparent depth wrong. `dFdx(h)`
captures the true on-screen rate of change however the chart is stretched. This path is also what makes
the fast preview follow the UV editor: move an island and its uv - hence its shading - moves with it
(the mesh rebuilds on drag-end, `on_island_edited(finished)``rebuild_preview()`
`rebuild_bump_preview_mesh()`). The branch is uniform and the paint weight gates by multiply, so the
texture derivatives stay well defined. A triangle straddling a seam has a discontinuous uv → the
`det≈0` guard skips it (a localised preview-only artifact, never in the bake).
**Parallax (triplanar path).** Perturbing the shading normal alone welds the pattern to the base surface:
it does not slide as the camera orbits, and does not get deeper as `depth_mm` grows. The triplanar path
therefore shades at the point the *displaced* surface would show at this pixel, found by **ray marching**
(parallax occlusion mapping). A point at ray parameter `s`, i.e. `P + V·s` (`P` the base point, `V` the
unit direction to the eye), sits at height `s·dot(V,n)` above the undisplaced surface. The displaced
surface lives in a shell between the extreme values of `amp·(h midlevel)` - taken from both ends of
`h ∈ [0,1]`, so it holds for an inverted layer and a raised midlevel too, where the surface sits *below*
the undisplaced one. The march starts at the top of that shell, where the ray is outside the surface by
construction, and steps inward until the ray height drops below the sampled height. That crossing *is*
the visible point.
Solving `Q = P + V·(H(Q)/dot(V,n))` by fixed-point iteration instead is geometrically exact but the
divisor goes to zero edge-on; the sample then lands a large fraction of a tile away and the iteration
oscillates, which reads as a second, flat copy of the pattern ghosted over the real one. Clamping the
step to one tile does not help - a tile-sized shift lands on the neighbouring tile, the same pattern
again. Offset limiting (stepping along the tangential part of `V`) is stable but understates parallax
enough that the relief still flattens as soon as the camera tilts. Marching has neither problem.
The hit is interpolated between the last two samples, which keeps `PARALLAX_STEPS` (24) affordable, and
the whole march is skipped when sweeping the shell would move the sample point less than half a texel -
the head-on case, so the common view pays almost nothing. The 140 variant samples with
`textureLod(…, 0.0)` inside the loop, since implicit derivatives are undefined in non-uniform control
flow. Two uniforms exist for this: `midlevel` (parallax needs the real height, not just its derivative)
and `eye_model_pos` (the camera in the volume's local frame).
Parallax cannot change the model's silhouette or cast shadows; the View row's Normal mode is one click
away for that. The LSCM path stays plain Mikkelsen bump - it has no closed-form uv, so there is no cheap
way to re-project a marched position. One further approximation: the GPU sampler's wrap mode stands in
for `tile_enabled`/`tile_method`, so with tiling *off* the GPU repeats where the CPU returns 0 outside
`[0,1)`.
### On-canvas "Adjust Texture" gizmo
A per-active-layer toggle ("Adjust placement") that disables painting and shows a flat pan panel (free
2D drag on both axes) plus two arrows along the patch's own U/V axes (constrained single-axis drag).
Anchored to the painted patch's centroid/average-normal (`compute_layer_paint_anchor()`). Hit-testing is
screen-space distance/point-to-segment, not real 3D ray intersection against the handle geometry - simple
and good enough at this handle size.
### Projection frame overlay (ViewProjected)
`src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - a semi-transparent, resizable `wxFrame` the user
drags **over the 3D view**, like a slide projector's gate. Whatever the model shows through it is what
the texture is projected onto, and the window's border becomes the hard edge of the displacement.
Press **Apply projection frame** and the gizmo reads the window's rectangle and commits it.
The window is deliberately **dumb**: it owns no placement state and reports nothing continuously. Its
position and size *are* the placement, read on demand at Apply - which is also when the expensive
visible-facet raycast runs. So dragging it is free and nothing recomputes until asked.
Plain 2D (`wxPaintDC`), not a `wxGLCanvas`: a second GL canvas would have to share the app's one real
`wxGLContext`. It only ever draws a bitmap and a border.
**The projective mapping (`apply_projection_frame()`)**. The frame defines a **screen-space** rectangle,
but the bake samples from a **local-space** position, so the two have to be reconciled.
`view_project_right/up` can only express an *affine* projection - exact under an orthographic camera, but
wrong under perspective, where the near end of a part projects larger than the far end and no pair of
axes reproduces that. So the layer instead stores a full projective map (`view_project_matrix`, row-major
3×4, `uv = (row0·p̃/row2·p̃, row1·p̃/row2·p̃)`), built like this:
- `K = projection · view · (instance · volume)`, i.e. local → clip, the same product the renderer uses.
Note `Camera::get_projection_matrix()` is typed `Transform3d` (nominally affine) but its perspective
form explicitly writes a `(0, 0, 1, 0)` bottom row into the underlying 4×4, so `clip.w = z_eye` is
genuinely carried. The build therefore multiplies **`.matrix()` products** (plain `Matrix4d`), never
`Transform3d` products, which would not compose that row correctly.
- Window coordinates follow `igl::project`'s convention (as `CameraUtils::project` does), with y
measured downward. Writing `uv = (win rect_origin) / rect_size` makes u and v affine in
`ndc = clip.xyz / clip.w`; multiplying through by `clip.w` leaves a plain linear combination of `K`'s
rows, which is exactly the 3×4 matrix - the perspective divide survives intact.
- `w > 0` is checked rather than divided blindly. A point behind the projector has `w < 0` and divides
to a plausible-looking but **mirrored** uv - the classic way a projected decal reappears on the back
of a model. `project_uv_projective()` returns false there and the caller treats it as no height.
The map already includes placement, so `apply_uv_transform()` is **not** applied on top of it - the
window's own position and size are the placement, and the tiling/rotation/offset sliders would shove
the result off the frame the user just aligned. A "Clear" button drops back to the affine path where
those controls mean something again.
Apply also sets `tile_enabled = false`, so `DecodedHeightTexture::sample()` returns 0 outside `[0,1)`
and the border is a hard edge rather than the first seam of an endless repeat, and repaints the layer
via `select_visible_faces(&matrix)` - the frame's uv square clips the selection, which both matches the
paint to the border and keeps the ray queries proportional to the framed area instead of the model.
Owned by the gizmo and **destroyed** (not just hidden) in `on_shutdown()`. Closing it only hides it, so
reopening keeps it where it was left.
### UV Editor pane
`UVEditorCanvas` (`src/slic3r/GUI/UVEditorCanvas.hpp/.cpp`) - a standalone `wxGLCanvas` rendering the
flattened LSCM islands (per-island wireframe + outline + fill) over the height texture (background
quad tiled across the whole unwrap), with mouse pan/zoom. It is wrapped in a **`UVEditorPanel`**
(same file) that adds a button row (Frame / Snap / Avg scale / Cut / Join / Unjoin) and a status line
along the bottom naming the current gesture and the shortcuts in play. The *panel* is what is
registered as a `wxAuiPaneInfo` pane on `Plater`'s `m_aui_mgr`; `Plater::show_uv_editor(bool)`
shows/hides it (deferred via `CallAfter`, since the gizmo calls it mid-3D-frame), and
`get_uv_editor_canvas()` returns the inner canvas the gizmo talks to.
Deliberately **shares the app's one real `wxGLContext`** (`wxGetApp().init_glcontext(*this)`, the
same call `View3D`/`Preview`/`AssembleView` make) rather than creating an independent context like
`SkipPartCanvas` does elsewhere in this codebase - this is what lets it reuse the already-registered
`"flat"`/`"flat_texture"` shaders and `GLModel` as-is, instead of needing its own shader
compilation/VBO management.
**Geometry is uploaded once, in the unwrap's own (raw, mm) coordinates**, one `GLModel` set per island;
each island is then drawn through its own 2x3 affine (`island_transform_matrix()` composed with the
layer's tiling/rotation/offset) passed as the `flat` shader's `view_model_matrix`. A drag updates one
matrix per island and touches no vertex buffer - `on_island_edited(!finished)` calls only
`set_island_transforms()`, and the full `set_islands()` rebuild happens solely when the unwrap itself
changes (`unwrap_changed` in `update_uv_editor()`).
**Gestures** (canvas-owned, reported to the gizmo as incremental deltas via `IslandEditFn`): left-drag
= move, right-drag or **R** = rotate (hold **Shift** to snap to 15° steps - quantised on the
*cumulative* rotation, not each delta, so it doesn't judder, and accumulated incrementally so it
survives crossing ±180°), **S** = scale (R/S modal, click/Enter to confirm, Esc to cancel), wheel =
zoom about the cursor, middle-drag = pan, **Home**/**F** = frame all. Scale writes
`TextureIsland::scale`; "Avg scale" (`average_island_scales()`) sets every island to the mean, so
one island scaled by hand can be matched back to its neighbours' texel density. **Snap** (canvas-owned
`m_snap_enabled`, toggled from the toolbar) sticks a dragged island's nearest boundary vertex onto a
neighbouring island's at drag-*end* only - a magnet that re-applies mid-drag is very hard to pull out
of. Toolbar commands the canvas can't service itself (Avg scale, Cut, Join, Unjoin) are forwarded to the
gizmo via `CommandFn`; view-only ones (Frame, Snap) it handles directly.
## File map
**libslic3r (core, no GUI dependency):**
- `src/libslic3r/TextureDisplacement.hpp/.cpp` - data model, bake algorithm, projection methods,
tiling, subdivision (uniform + adaptive longest-edge bisection), post-process smoothing
(`smooth_mesh_vertices()`), and `TextureDisplacementOptions` (the whole-stack settings). See doc
comments throughout, they're kept accurate and up to date.
- `src/libslic3r/MeshBoolean.hpp/.cpp` - `parameterize_lscm()` and `remesh_isotropic()` in the `cgal`
sub-namespace, reusing the existing `CGALMesh`/`_EpicMesh`/conversion-helper infrastructure already
there for mesh boolean ops. CGAL includes: `Polygon_mesh_processing/border.h`,
`Polygon_mesh_processing/connected_components.h`, `Surface_mesh_parameterization/{Error_code,
LSCM_parameterizer_3, parameterize}.h`. No new dependency - CGAL 5.6.3 is already vendored and the
`Surface_mesh_parameterization` package headers were already present.
- `src/libslic3r/Model.hpp/.cpp` - the 8 named `FacetsAnnotation` fields + accessor,
`texture_displacement_layers`, `texture_displacement_options`, and all the mirrored touch points
(see Data model above).
**GUI:**
- `src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp/.cpp` - the gizmo and its whole panel.
- `src/slic3r/GUI/TextureLibrary.hpp/.cpp` - scans the shipped + user texture folders, imports an
arbitrary image into the user folder (converting it to the 8-bit grayscale PNG libslic3r decodes),
and loads a library file's bytes for a layer. The image→grayscale-PNG conversion lives here, on the
GUI side, because libslic3r has no image toolkit; both the import path and the "pick a shipped
texture" path go through the same one function.
- `resources/textures/displacement/*.png` - the 10 shipped height maps (Bricks, Grid, Hexagons,
Knurl, Noise, Quilt, Studs, Waves, Weave, Wood Grain). All 512×512 8-bit grayscale and **seamless**
(each is periodic over the full image in both axes, so tiling shows no seam). Generated
procedurally; the whole `resources/` tree is installed recursively by CMake, so a new folder under
it ships with no build-system change.
- `src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.hpp/.cpp` - background bake commit.
- `src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.hpp/.cpp` - background preview compute
(mirrors the bake job's shape but commits nothing to the Model).
- `src/slic3r/GUI/TextureProjectorFrame.hpp/.cpp` - the semi-transparent projection-frame overlay for
ViewProjected layers (plain 2D `wxPaintDC`, no GL context - see its section above).
- `src/slic3r/GUI/UVEditorCanvas.hpp/.cpp` - the 2D UV unwrap viewer widget.
- `src/slic3r/GUI/Plater.hpp/.cpp` - `uv_editor_canvas` member, AUI pane registration,
`get_uv_editor_canvas()`/`show_uv_editor()`.
- `src/slic3r/GUI/GLShadersManager.cpp` - registers `"texture_displacement_bump"`.
- `resources/shaders/{110,140}/texture_displacement_bump.{vs,fs}` - the fast-preview shader.
- `src/slic3r/GUI/Gizmos/GLGizmoPainterBase.hpp` - `PainterGizmoType::TEXTURE_DISPLACEMENT`.
- `src/slic3r/GUI/Gizmos/GLGizmosManager.hpp/.cpp` - `EType::TextureDisplacement` registration.
## Tests
`tests/libslic3r/test_texture_displacement.cpp`. Covers `decode_height_texture` round-trip, empty-layer
no-op, full-cube uniform displacement, a second layer over the same area contributing, all four blend
modes (table-driven), the lowest layer ignoring its blend mode, border displace/pin, post-process
smoothing and its mask guarantees, and adaptive subdivision: conformality (`every_edge_used_twice` on a
partially-refined cube - an exact crack detector for a closed mesh), the target edge length actually
being reached, the triangle budget capping the result without opening a crack, curvature-driven
refinement (a Gaussian bump refines at its centre, a linear ramp adds nothing), and the max-edge
baseline.
`BUILD_TESTS` is `OFF` in the checked-in build cache; flip it on to run them:
cmake -S . -B build -DBUILD_TESTS=ON
cmake --build build --config Release --target libslic3r_tests -- -m
./build/tests/libslic3r/Release/libslic3r_tests.exe "[TextureDisplacement]" --order rand

View File

@@ -0,0 +1,326 @@
# Texture Displacement — Feature & Controls Guide
Texture Displacement is a paint-style gizmo that stamps height-map textures onto a model's surface
and turns them into real relief — engraved or embossed detail — either as a live preview or baked
into actual mesh geometry. You paint where the texture applies, stack multiple textures as blended
layers, choose how each is projected onto the surface, and (for the unwrap projection) lay the result
out by hand in a dedicated 2D **UV Editor** pane.
This document describes every feature and control. For the internal architecture and algorithms, see
`TEXTURE_DISPLACEMENT.md`.
---
## Table of contents
1. [Quick start](#quick-start)
2. [Entering the tool](#entering-the-tool)
3. [Selection modes](#selection-modes)
4. [View modes](#view-modes)
5. [Auto update](#auto-update)
6. [Texture layers](#texture-layers)
7. [Per-layer settings](#per-layer-settings)
8. [Projection methods](#projection-methods)
9. [The UV Editor](#the-uv-editor)
10. [Seams](#seams)
11. [Adjust placement (on-model)](#adjust-placement-on-model)
12. [Preparing the mesh: Subdivide & Remesh](#preparing-the-mesh-subdivide--remesh)
13. [Baking & resetting](#baking--resetting)
14. [Controls reference](#controls-reference)
15. [Tips & limitations](#tips--limitations)
---
## Quick start
1. Select an object and open the **Texture displacement** gizmo from the left toolbar.
2. A texture layer is added automatically. Pick a texture from the layer's picker, or import your own.
3. **Paint** the area you want the texture to affect (or press **Select whole model**).
4. The relief appears live on the model. Tune **Depth**, **Tile size**, **Rotation**, etc.
5. If the model is low-poly, use **Subdivide** or **Remesh** so there are enough vertices for detail.
6. Press **Bake** to convert the preview into real geometry, or leave it as a live preview.
> The tool only ever affects the **painted** area. Everything you don't paint keeps its original
> surface, and bake blends the relief seamlessly into it.
---
## Entering the tool
The gizmo lives on the left gizmo toolbar (icon: `toolbar_texture_displacement.svg`). Its settings
panel opens beside the toolbar. You can **Dock panel / Undock panel** (top of the panel) to pin it or
float it freely over the 3D view, and **Close** at the bottom exits the gizmo.
When you first open the tool on a never-textured object it starts with **one texture layer already
added**, so you can paint straight away.
---
## Selection modes
Choose *how* you paint. All three write into the **active layer's** mask.
| Mode | What it does |
|------|--------------|
| **Brush** | Free-hand painting with a round brush. Shows a **Brush size** slider and a **Circle / Sphere** choice (circle = surface disc, sphere = 3D ball that also paints around curves). |
| **Face** | Click a single triangle to paint it. |
| **Connected area** | Click to flood-fill a region; the **Angle threshold** slider limits how far the fill spreads across changes in surface angle. |
- **Select whole model** — marks the entire model as painted for the active layer, instead of
brushing it by hand.
---
## View modes
A row of icon buttons labelled **View** controls how the painted area is shown. The first four are a
radio group; **Wireframe** is an independent toggle. Hover any icon for its tooltip.
| View | Meaning |
|------|---------|
| **Normal** | The true displaced geometry — exactly what **Bake** produces. Rebuilt in the background. |
| **Fast** | A GPU bump-shaded approximation of the *active layer only*. No real geometry movement — quick to update, not exact. Best while tuning or dragging islands. |
| **Checker** | A test grid painted over the unwrap so you can see stretching (squares stay square where the map isn't distorted). |
| **Distortion** | A blue→green→red heatmap of how much each area is compressed or stretched in UV space. Needs the **Unwrap (LSCM)** projection. |
| **Wireframe** | Overlays the mesh edges (white). Independent of the view above; in **Normal** view it sits on the displaced surface. |
---
## Auto update
**Auto update** (on by default) rebuilds the true displaced geometry as soon as *anything* changes —
painting, swapping textures, moving sliders. Turn it off on very heavy models to only rebuild when you
release a slider (painting still updates on stroke end).
---
## Texture layers
You can stack up to **8** texture layers. Each has its own independent paint mask, its own texture,
and its own parameters, and they combine in slot order like layers in an image editor.
- **Add a layer** — the ** icon** to the right of the *Texture layers* heading (reuses the tool icon
for now).
- **Remove** — the button on each layer's header row.
- **Active layer** — click a layer's header (or anywhere in its block) to make it active. The active
layer is the one you paint into and the one whose block is tinted. Only one layer is active at a time.
- **Erase all** — clears the active layer's paint.
Each layer shows a texture **picker** (large preview + name). Open it to choose from the shipped
library or import your own image (any png/jpg/bmp; it's converted to an 8-bit grayscale height map and
copied into your user texture folder so app updates can't overwrite it).
---
## Per-layer settings
| Control | Range / options | What it does |
|---------|-----------------|--------------|
| **Depth (mm)** | 0.0110 (log) | Maximum displacement along the surface normal. |
| **Tile size (mm)** | 0.2200 (log) | Physical size of one texture tile on the surface. |
| **Rotation** | 0360° | Rotates the texture on the surface. |
| **Midlevel** | 010 | The grey level that means "don't move". At 0 the texture only pushes outward; raise it and darker texels cut *inward* (one map both embosses and engraves). 0.5 makes mid-grey neutral. |
| **Smoothing** | 01 | Blurs the height texture before it displaces — rounds hard edges and removes speckle without needing a softer source image. |
| **Edge smoothing** | checkbox + **Edge amount** 01 | Fades the relief to flat toward the *edge of the painted area*, so it blends into the surrounding surface. A small amount softens only a thin band at the very edge; the maximum flattens the whole painted face. |
| **Invert** | checkbox | Flips the height map (peaks become valleys). |
| **Blend** | Add / Subtract / Multiply / Divide | How this layer combines with the layers **below** it where they overlap. Add/Subtract pile relief on or carve it away; Multiply/Divide scale the relief underneath (a mask). The lowest painted layer is the **Base** and always behaves additively. |
| **Tile** | checkbox + **Repeat / Mirrored repeat** | When off, the texture is placed once (a decal) instead of repeating. Mirrored repeat flips every other tile to hide seams. |
| **Projection** | see below | How the texture is mapped onto the painted surface. |
> **Midlevel warning:** cutting inward can fold the surface through itself in sharp concave corners or
> thin walls. Keep Depth small relative to the feature you're cutting into; the panel warns when a deep
> inward setting is risky.
---
## Projection methods
How the 2D texture is wrapped onto the 3D painted area.
| Method | Best for | Notes |
|--------|----------|-------|
| **Triplanar (blended)** | Patches wrapping around edges | Projects from all three axes at once and blends, so there's no seam across a sharp edge. |
| **Cylindrical** | Round, tube-like selections | Wraps the texture around the patch's own centre/axis. |
| **Spherical** | Ball-like selections | Longitude/latitude wrap around the patch centre. |
| **Unwrap (LSCM)** | Flat, controlled layout | A real conformal unwrap. Cuts the area into pieces at sharp edges (see **Seam angle**), flattens each, and lets you lay them out by hand in the **UV Editor**. Unlocks Checker/Distortion, seams, and island editing. |
| **From view** | Decals / slide-projector look | Projects straight onto the surface from the current camera direction. Use **Capture current view** to re-lay it from wherever you're looking. |
### LSCM-only controls
These appear when a layer uses **Unwrap (LSCM)**:
- **Seam angle** (590°) — edges sharper than this are cut so each piece lies flat. Lower cuts more
(less stretching, more seams); raise to keep more in one piece. A box's 90° corners are cut by
default. *Ignored once you've marked any seam by hand* (your seams then define the pieces).
- **Connect islands** (on by default) — lays the unwrap out as a **connected net**: pieces that share
an edge are unfolded next to each other (a cube becomes a joined net instead of six loose squares).
They stay separate islands, so you can still move any of them by hand. Turn off for the classic
packed-grid layout.
- **Open UV editor** — shows the flattened unwrap in a side pane (see below). Opens *only* when you
turn this on — it never pops up on its own.
- **Mark seams** / **Path** / **Clear seams** — see [Seams](#seams).
- An **Unwrap: N islands, F faces, V verts** read-out tells you what the unwrap actually produced.
---
## The UV Editor
A dockable 2D pane (enable **Open UV editor** on an LSCM layer) showing the flattened unwrap over the
height texture. Islands are the flattened pieces; you can rearrange them freely — nothing re-packs them
behind your back. Moving an island updates the model **live** (in Fast view it tracks the cursor
smoothly, via a shader uniform — no rebuild until you release).
### Navigation
| Action | Control |
|--------|---------|
| Pan | Middle-drag |
| Zoom | Mouse wheel (zooms about the cursor) |
| Frame everything | **Home** or **F**, or the **Frame** toolbar button |
### Editing an island
| Action | Control |
|--------|---------|
| Select | Left-click an island |
| Move | Left-drag |
| Rotate | Right-drag, or press **R** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Rotate snapped | Hold **Shift** while rotating — snaps to **global** 15° marks (0/15/30…). A protractor dial with tick marks and the current angle is shown. |
| Scale | Press **S** then move the mouse (click/Enter to confirm, Esc to cancel) |
| Undo / Redo | **Ctrl+Z** / **Ctrl+Shift+Z** or **Ctrl+Y** |
The **selected** island gets a bold light-green outline and a brighter wireframe; unselected islands
are a translucent light-green wash. The texture underneath repeats exactly as it will when baked.
A **status line** along the bottom always names the current gesture and the shortcuts in play.
### Toolbar
| Button | Action |
|--------|--------|
| **Frame** | Frame all islands (same as Home). |
| **Snap** | Toggle magnetic snapping — a dragged island sticks its boundary to a neighbour's when they come close. |
| **Avg scale** | Give every island the same texel density (Blender's "Average Islands Scale"). |
| **Cut** | Split the selected island across its long axis (useful for very long islands). |
| **Join** | Unfold the selected island onto its nearest neighbour along their shared edge — keeps both as separate islands with their own borders. |
| **Unjoin** | Send the selected island back to its own packed position. |
> **Checker / Distortion in the UV editor:** selecting those View modes also colours the UV pane — a
> checker background, or a per-island distortion heatmap — so you can judge stretch in 2D as well as
> on the model.
---
## Seams
Seams are edges the unwrap is forced to cut along, on top of whatever the Seam angle cuts — the
Blender "mark seam" workflow. They let you control exactly where the unwrap splits.
Enable **Mark seams** on an active LSCM layer, then:
- **Click an edge** on the model to mark it (it turns **red**); click a red edge again to unmark it.
The edge under the cursor is highlighted **yellow** so you can see what a click will toggle.
- **Path mode** (the **Path** checkbox) — for dense meshes where clicking each edge is tedious: click a
start point, then an end point, and the whole **shortest path** between them is seamed at once. It
chains (each click extends from the last point); the start vertex is shown in **green**.
- **Ctrl+drag** rotates/pans the camera while in seam mode.
- **Clear seams** removes them all.
Once any seam is marked, the automatic Seam-angle cutting is disabled so *your* seams define the
islands — pieces you leave un-seamed merge together.
---
## Adjust placement (on-model)
**Adjust placement** (on an active layer) lets you position the texture by dragging a handle on the
model instead of nudging the Rotation/offset numbers. The handle is a flat panel in the patch's
tangent plane (drag anywhere on it to move freely) plus U/V arrows for single-axis nudges. It's
anchored to the painted patch, so paint something first.
---
## Preparing the mesh: Subdivide & Remesh
Displacement can only move vertices that exist, so a coarse model needs more of them first.
### Subdivide
Splits every triangle into four, **15 times** (each step roughly quadruples the triangle count).
- **Subdivide steps** (15) — how many times to split.
- **Preview subdivision** — shows the result as a **cyan wireframe** without changing the model.
- **Apply** — commits the subdivision to the geometry.
- **Done** — ends the preview and leaves the model as it is.
### Remesh
Rebuilds the whole model with triangles close to a target edge length — evens out a mesh with wildly
varying triangle sizes (CGAL isotropic remeshing).
- **Target edge (mm)** — desired triangle edge length (seeded to the model's current average).
- **Remesh** — splits the big triangles and merges the small ones to that size.
> Both Subdivide-Apply and Remesh **replace the geometry** and clear any *not-yet-baked* paint on it
> (already-baked relief is kept). If you had the mesh **Wireframe** on before, it stays on afterward.
---
## Baking & resetting
- **Bake** — converts the current preview into real, permanent mesh geometry, restricted to the
painted area. Runs in the background; the button shows *Baking…* while it works.
- **Erase all** — clears the active layer's paint.
Baking is the exact same algorithm as the **Normal** preview, so what you see is what you get.
---
## Controls reference
### Mouse — 3D view (while painting)
| Input | Action |
|-------|--------|
| Left-drag | Paint the active layer |
| Ctrl + drag | Rotate / pan the camera (works in seam mode too) |
| Wheel | Zoom |
### Mouse & keys — UV Editor
| Input | Action |
|-------|--------|
| Left-click | Select island |
| Left-drag | Move island |
| Right-drag | Rotate island |
| **R** / **S** | Modal rotate / scale (mouse drives it, click or Enter confirms, Esc cancels) |
| **Shift** (while rotating) | Snap to global 15° marks |
| Middle-drag | Pan |
| Wheel | Zoom about cursor |
| **Home** / **F** | Frame all islands |
| **Ctrl+Z** / **Ctrl+Shift+Z** / **Ctrl+Y** | Undo / redo |
### Seam mode
| Input | Action |
|-------|--------|
| Click edge | Mark / unmark a seam (yellow = hover, red = marked) |
| Click (Path mode) | Set start, then seam the shortest path to the next click |
| Ctrl + drag | Rotate / pan camera |
---
## Tips & limitations
- **Paint first, then bake.** The preview is free to explore; only Bake changes the real mesh.
- **Not enough detail?** Subdivide or Remesh before painting fine textures.
- **Inward cuts** (high Midlevel + big Depth) can self-intersect on thin walls or sharp concave
corners — keep Depth modest there.
- **Fast vs Normal:** Fast preview shades a bump and shows only the active layer; use it for quick
tuning and smooth UV dragging, but trust **Normal**/**Bake** for the exact result.
- **Topology changes drop unbaked paint.** Subdivide-Apply, Remesh, and Simplify replace the mesh, and
texture-displacement paint isn't remapped across that change (already-baked relief is unaffected).
- **Island placements** are tied to the current unwrap. Re-painting or changing the Seam angle can
re-segment the charts and renumber them, so a re-unwrap re-lays the connected net and discards
hand placements made before it.
- **Connect islands** is on by default; turn it off (per layer) for the classic packed-grid layout, or
if an unfold looks wrong on an unusual mesh.

View File

@@ -4,7 +4,7 @@ set -e
set -o pipefail
SECONDS=0
while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
while getopts ":dpa:snt:xbc:i:1Tuh" opt; do
case "${opt}" in
d )
export BUILD_TARGET="deps"
@@ -38,8 +38,8 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
i )
export CMAKE_IGNORE_PREFIX_PATH="${CMAKE_IGNORE_PREFIX_PATH:+$CMAKE_IGNORE_PREFIX_PATH;}$OPTARG"
;;
j )
export CMAKE_BUILD_PARALLEL_LEVEL="$OPTARG"
1 )
export CMAKE_BUILD_PARALLEL_LEVEL=1
;;
T )
export BUILD_TESTS="1"
@@ -58,7 +58,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -b: Build without reconfiguring CMake"
echo " -c: Set CMake build configuration, default is Release"
echo " -i: Add a prefix to ignore during CMake dependency discovery (repeatable), defaults to /opt/local:/usr/local:/opt/homebrew"
echo " -j: Set the number of parallel build jobs (CMAKE_BUILD_PARALLEL_LEVEL)"
echo " -1: Use single job for building"
echo " -T: Build and run tests (set ORCA_TESTS_BUILD_ONLY=1 to build without running)"
exit 0
;;

View File

@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_add.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="16"
inkscape:cx="1.625"
inkscape:cy="10.84375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" />
<path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" />
<path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" />
<path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" />
<path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" />
<path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" />
<path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" />
<rect
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect1"
width="22.449276"
height="1.000026"
x="8.7753611"
y="19.499987"
ry="0.50001299" />
<path
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect2"
width="22.449276"
height="1.000026"
x="8.775362"
y="-20.500011"
transform="rotate(90)"
sodipodi:type="rect"
inkscape:path-effect="#path-effect3"
d="M 9.2741959,-20.500011 H 30.737917 a 0.48672135,0.48672135 45 0 1 0.486721,0.486721 0.51978097,0.51978097 135.70952 0 1 -0.526178,0.513305 H 9.2753766 A 0.50001454,0.50001454 45 0 1 8.775362,-20 v -0.0012 a 0.49883385,0.49883385 135 0 1 0.4988339,-0.498833 z" />
</svg>

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@@ -0,0 +1,109 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_add_negative.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="45.254834"
inkscape:cx="16.46233"
inkscape:cy="19.301805"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" />
<path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" />
<path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" />
<path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" />
<path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" />
<path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" />
<path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" />
<rect
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect1"
width="22.449276"
height="1.000026"
x="8.7753611"
y="19.499987"
ry="0.50001299" />
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,330 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_group.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="25.28125"
inkscape:cy="18.359375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><g
id="g31"
transform="translate(-1.2926181,-1.328125)"><path
d="m 11.472656,12.726388 v 6.047224 A 1.2263883,1.2263883 45 0 0 12.699044,20 h 6.074568 A 1.2263883,1.2263883 135 0 0 20,18.773612 V 12.726388 A 1.2263883,1.2263883 45 0 0 18.773612,11.5 h -6.074568 a 1.2263883,1.2263883 135 0 0 -1.226388,1.226388 z m 1.730947,-0.0096 h 5.065451 a 0.47508735,0.47508735 45 0 1 0.475087,0.475087 v 5.303732 a 0.47508735,0.47508735 135 0 1 -0.475087,0.475087 h -5.065451 a 0.47508735,0.47508735 45 0 1 -0.475087,-0.475087 l 0,-5.303732 a 0.47508735,0.47508735 135 0 1 0.475087,-0.475087 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path30"
inkscape:path-effect="#path-effect31"
inkscape:original-d="M 11.472656,11.5 V 20 H 20 v -8.5 z m 1.25586,1.216797 h 6.015625 v 6.253906 h -6.015625 z" /><path
d="m 11.472656,12.726388 v 6.047224 A 1.2263883,1.2263883 45 0 0 12.699044,20 h 6.074568 A 1.2263883,1.2263883 135 0 0 20,18.773612 V 12.726388 A 1.2263883,1.2263883 45 0 0 18.773612,11.5 h -6.074568 a 1.2263883,1.2263883 135 0 0 -1.226388,1.226388 z m 1.730947,-0.0096 h 5.065451 a 0.47508735,0.47508735 45 0 1 0.475087,0.475087 v 5.303732 a 0.47508735,0.47508735 135 0 1 -0.475087,0.475087 h -5.065451 a 0.47508735,0.47508735 45 0 1 -0.475087,-0.475087 l 0,-5.303732 a 0.47508735,0.47508735 135 0 1 0.475087,-0.475087 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path30-4"
inkscape:path-effect="#path-effect31-9"
inkscape:original-d="M 11.472656,11.5 V 20 H 20 v -8.5 z m 1.25586,1.216797 h 6.015625 v 6.253906 h -6.015625 z"
transform="translate(11.058594)" /><path
d="m 11.472656,12.726388 v 6.047224 A 1.2263883,1.2263883 45 0 0 12.699044,20 h 6.074568 A 1.2263883,1.2263883 135 0 0 20,18.773612 V 12.726388 A 1.2263883,1.2263883 45 0 0 18.773612,11.5 h -6.074568 a 1.2263883,1.2263883 135 0 0 -1.226388,1.226388 z m 1.730947,-0.0096 h 5.065451 a 0.47508735,0.47508735 45 0 1 0.475087,0.475087 v 5.303732 a 0.47508735,0.47508735 135 0 1 -0.475087,0.475087 h -5.065451 a 0.47508735,0.47508735 45 0 1 -0.475087,-0.475087 l 0,-5.303732 a 0.47508735,0.47508735 135 0 1 0.475087,-0.475087 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path30-4-3"
inkscape:path-effect="#path-effect31-9-1"
inkscape:original-d="M 11.472656,11.5 V 20 H 20 v -8.5 z m 1.25586,1.216797 h 6.015625 v 6.253906 h -6.015625 z"
transform="translate(0,11.15625)" /></g></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,165 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_checker.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect251"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.896875,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect229"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,0.49844035,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.49844035,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect206"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0051443,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0058593,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0051442,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.003906,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect121"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.97194886,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.9540633,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.94238026,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="22.627417"
inkscape:cx="45.519999"
inkscape:cy="5.4579805"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path251"
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="m 7.8085938,8.3265625 v 0.103125 10.8320315 1 10.832031 a 1,1 45 0 0 1,1 h 10.8320312 1 10.832031 a 1,1 135 0 0 1,-1 v -10.832031 -1 -10.8320315 a 1,1 45 0 0 -1,-1 h -10.832031 -1 -10.8320312 -0.103125 a 0.896875,0.896875 135 0 0 -0.896875,0.896875 z M 20.640625,8.4296875 H 31.472656 V 19.261719 H 20.640625 Z M 8.8085938,20.261719 H 19.640625 V 31.09375 H 8.8085938 Z"
inkscape:path-effect="#path-effect251"
inkscape:original-d="m 7.8085938,7.4296875 v 1 10.8320315 1 10.832031 1 h 1 10.8320312 1 10.832031 1 v -1 -10.832031 -1 -10.8320315 -1 h -1 -10.832031 -1 -10.8320312 z m 12.8320312,1 H 31.472656 V 19.261719 H 20.640625 Z M 8.8085938,20.261719 H 19.640625 V 31.09375 H 8.8085938 Z" />
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

View File

@@ -0,0 +1,115 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_connected_area.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect121"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.97194886,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.9540633,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.94238026,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="5.90625"
inkscape:cy="27.609375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path89"
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="m 7.8638887,10.084132 c 0.00101,0.01203 0.00272,0.02557 0.00526,0.04087 l 0.035156,0.203125 2.1367187,12.847656 0.214843,1.289063 0.982422,5.90039 0.0039,0.0293 0.002,0.04492 a 1.1174189,1.1174189 31.375688 0 0 1.274062,0.776949 l 1.309971,-0.08942 0.283199,-0.01758 0.265624,-0.01563 15.117188,-1.078125 a 1.107441,1.107441 135.10954 0 0 1.023438,-1.019532 l 0,0 0.427734,-5.447266 0.02539,-0.322266 0.06055,-0.755859 0.02344,-0.300781 0.789062,-10.052734 0.0059,-0.08203 c 5.2e-5,-6.47e-4 1.04e-4,-0.0013 1.56e-4,-0.0019 0.04173,-0.518787 -0.346547,-0.972695 -0.865395,-1.013686 L 20.908203,10.216797 19.798828,10.128906 8.6777344,9.2421875 C 8.1437829,9.2001186 7.8209269,9.5718807 7.8638887,10.084132 Z m 2.1575953,0.212743 8.246094,0.65625 0.863281,0.06836 -1.660156,2.21875 -0.113281,0.152344 -0.09766,0.128906 -0.128907,0.173828 -0.238281,-0.113281 -3.402344,-1.626953 z m -1.0800778,0.527344 6.3183598,3.021484 1.294922,0.619141 -0.109376,0.146484 -5.623046,7.519531 z m 11.2988278,0.285156 7.986328,0.636719 -9.626953,1.558594 z M 30.886719,12.271484 30.132812,21.875 25.191406,18.59375 19.574219,14.863281 18.654297,14.253906 Z m -12.990235,2.609375 1.208985,0.802735 10.603515,7.042968 -16.998046,7.029297 5.11914,-14.68164 z m -1.455078,1.310547 -4.472656,12.826172 -0.931641,-5.597656 z m 13.552735,7.4375 -0.425782,5.4375 -15.425781,1.119141 z m -17.699219,7.320313 -0.0039,0.0098 v -0.0078 z"
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
inkscape:original-d="M 7.7109375,9.1660156 C 8.1218019,10.255395 7.816209,9.8053782 7.8691443,10.125 l 0.035156,0.203125 2.1367187,12.847656 0.214843,1.289063 0.982422,5.90039 0.0039,0.0293 0.002,0.04492 0.152345,0.853515 2.431688,-0.165982 0.283199,-0.01758 0.265624,-0.01563 15.117188,-1.078125 0.949218,-0.06836 0.07422,-0.951172 0.427734,-5.447266 0.02539,-0.322266 0.06055,-0.755859 0.02344,-0.300781 0.789062,-10.052734 0.0059,-0.08203 c 0.02524,-0.313762 0.04951,-0.627607 0.07421,-0.941408 L 30.984458,11.019565 20.908203,10.216797 19.798828,10.128906 8.6777344,9.2421875 Z m 2.3105465,1.1308594 8.246094,0.65625 0.863281,0.06836 -1.660156,2.21875 -0.113281,0.152344 -0.09766,0.128906 -0.128907,0.173828 -0.238281,-0.113281 -3.402344,-1.626953 z m -1.0800778,0.527344 6.3183598,3.021484 1.294922,0.619141 -0.109376,0.146484 -5.623046,7.519531 z m 11.2988278,0.285156 7.986328,0.636719 -9.626953,1.558594 z M 30.886719,12.271484 30.132812,21.875 25.191406,18.59375 19.574219,14.863281 18.654297,14.253906 Z m -12.990235,2.609375 1.208985,0.802735 10.603515,7.042968 -16.998046,7.029297 5.11914,-14.68164 z m -1.455078,1.310547 -4.472656,12.826172 -0.931641,-5.597656 z m 13.552735,7.4375 -0.425782,5.4375 -15.425781,1.119141 z m -17.699219,7.320313 -0.0039,0.0098 v -0.0078 z"
inkscape:path-effect="#path-effect121" />
</svg>

After

Width:  |  Height:  |  Size: 8.0 KiB

View File

@@ -0,0 +1,121 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_cross.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="16"
inkscape:cx="56.75"
inkscape:cy="8.65625"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" />
<path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" />
<path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" />
<path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" />
<path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" />
<path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" />
<path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" />
<rect
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect3"
width="24.968962"
height="0.96882033"
x="15.762758"
y="-0.50001258"
ry="0"
rx="0.50001299"
transform="rotate(45)" />
<rect
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect4"
width="24.968962"
height="0.96882033"
x="-12.500083"
y="-28.731649"
ry="0.50001299"
rx="0.50001299"
transform="rotate(135)" />
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

View File

@@ -0,0 +1,359 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_cut.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="15.375"
inkscape:cy="16.140625"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
d="m 16.082031,8.7625 v 22.475 a 0.965625,0.965625 45 0 0 0.965625,0.965625 h 5.85 A 0.965625,0.965625 135 0 0 23.863281,31.2375 V 8.7625 A 0.965625,0.965625 45 0 0 22.897656,7.796875 h -5.85 A 0.965625,0.965625 135 0 0 16.082031,8.7625 Z m 1.325781,-0.032031 h 5.184376 a 0.3257813,0.3257813 45 0 1 0.325781,0.3257813 V 30.94375 a 0.3257813,0.3257813 135 0 1 -0.325781,0.325781 H 17.407812 A 0.3257813,0.3257813 45 0 1 17.082031,30.94375 V 9.0562501 a 0.3257813,0.3257813 135 0 1 0.325781,-0.3257813 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path33"
inkscape:path-effect="#path-effect34"
inkscape:original-d="m 16.082031,7.796875 v 24.40625 h 7.78125 V 7.796875 Z m 1,0.9335938 h 5.835938 V 31.269531 h -5.835938 z" /><path
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect32"
width="1.0000267"
height="22.719467"
x="27.527292"
y="-12.430544"
transform="rotate(47.337307)"
inkscape:path-effect="#path-effect33"
sodipodi:type="rect"
d="m 28.027306,-12.430544 c 0.277007,0 0.500013,0.223006 0.500013,0.500013 V 9.7889099 c 0,0.2770071 -0.223006,0.5000131 -0.500013,0.5000131 -0.277008,0 -0.500014,-0.223006 -0.500014,-0.5000131 V -11.930531 c 0,-0.277007 0.223006,-0.500013 0.500014,-0.500013 z"
ry="0.50001335" /></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_distortion.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect447"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.92418405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.7580314,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.3849613,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5585937,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,0.39312043,0,1 @ F,0,0,1,0,0.39312043,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.39312043,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.53242312,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.32768312,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect446"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect445"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5585937,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect229"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,0.49844035,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.49844035,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect206"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0051443,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0058593,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0051442,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.003906,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect121"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.97194886,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.9540633,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.94238026,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="45.254834"
inkscape:cx="19.489631"
inkscape:cy="18.329534"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path392"
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="M 19.810547 7.4824219 A 4.0262158 4.0262158 0 0 0 18.550781 7.7421875 L 10.164062 10.806641 L 9.8007812 10.935547 A 1.2813952 1.2813952 0 0 0 8.9648438 11.9375 L 8.9023438 12.318359 L 8.3183594 15.894531 L 7.8710938 18.640625 A 4.753955 4.753955 0 0 0 7.8085938 19.40625 L 7.8085938 19.746094 L 7.8085938 19.761719 L 7.8085938 20.261719 L 7.8085938 20.642578 L 7.8085938 21.146484 L 7.8085938 26.261719 L 7.8085938 31.09375 L 7.8085938 31.236328 L 7.8085938 31.535156 A 0.5585937 0.5585937 0 0 0 8.3671875 32.09375 L 8.8085938 32.09375 L 18.650391 32.09375 L 19.640625 32.09375 L 20.064453 32.09375 L 20.640625 32.09375 L 31.472656 32.09375 A 1 1 0 0 0 32.472656 31.09375 L 32.472656 26.03125 L 32.472656 21.408203 A 4.849149 4.849149 0 0 0 32.410156 20.626953 L 32.349609 20.261719 L 32.347656 20.261719 L 32.283203 19.873047 L 32.183594 19.261719 L 32.15625 19.085938 L 32.085938 18.658203 L 31.472656 14.904297 L 31.050781 12.318359 L 30.953125 11.714844 A 0.90002024 0.90002024 0 0 0 30.365234 11.011719 L 29.816406 10.816406 L 29.789062 10.806641 L 21.207031 7.7480469 L 20.875 7.625 A 2.3640678 2.3640678 0 0 0 20.240234 7.4882812 A 4.0262158 4.0262158 0 0 0 19.810547 7.4824219 z M 20.640625 8.6074219 L 28.939453 11.564453 L 20.640625 18.652344 L 20.640625 8.6074219 z M 18.722656 8.8183594 L 8.8808594 18.660156 L 9.9785156 11.933594 L 18.722656 8.8183594 z M 19.640625 9.3144531 L 19.640625 19.261719 L 9.6933594 19.261719 L 19.640625 9.3144531 z M 29.984375 11.988281 L 31.171875 19.261719 L 31.173828 19.261719 L 31.216797 19.525391 L 31.335938 20.261719 L 31.333984 20.261719 L 31.214844 19.527344 L 31.171875 19.261719 L 21.466797 19.261719 L 29.984375 11.988281 z M 31.472656 19.261719 L 31.480469 19.261719 L 31.472656 19.269531 L 31.472656 19.261719 z M 8.8085938 20.261719 L 18.783203 20.261719 L 8.8085938 30.236328 L 8.8085938 25.404297 L 8.8085938 20.261719 z M 20.640625 20.261719 L 30.480469 20.261719 L 20.640625 30.103516 L 20.640625 20.261719 z M 31.414062 20.742188 L 31.472656 21.09375 L 31.472656 31.09375 L 21.064453 31.09375 L 31.412109 20.744141 L 31.414062 20.742188 z M 19.640625 20.818359 L 19.640625 31.09375 L 19.650391 31.09375 L 19.640625 31.103516 L 19.640625 31.09375 L 9.3671875 31.09375 L 19.640625 20.818359 z " />
</svg>

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,132 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_fast_preview.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect206"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0051443,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0058593,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0051442,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.003906,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect121"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.97194886,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.9540633,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.94238026,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="18.875"
inkscape:cy="8.734375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path150"
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="m 10.849609,11.300781 -0.208984,2.06836 -0.101563,1 -0.259765,2.546875 -0.101563,1 -0.2695309,2.664062 -0.1015625,1 -0.2714844,2.66211 -0.1015624,1 -0.2578126,2.546874 -0.1015624,1 -0.2558594,2.507813 a 0.90855311,0.90855311 47.877062 0 0 0.9042968,1 h 0 3.9492188 1.001953 4.632813 1 4.964843 1.001954 4.021484 l 7.15e-4,0 a 0.90823039,0.90823039 132.10041 0 0 0.903582,-1 l -0.25586,-2.507813 -0.101562,-1 -0.257813,-2.546874 -0.101562,-1 -0.271484,-2.66211 -0.101563,-1 -0.269531,-2.664062 -0.101563,-1 -0.259765,-2.546875 -0.101563,-1 -0.210937,-2.06836 -1.25e-4,-0.0012 a 1.1110301,1.1110301 42.100377 0 0 -1.105344,-0.998768 h -3.390625 -1.003906 -3.359375 -1 -3.027344 -1.001953 -3.320313 -7.15e-4 a 1.1124005,1.1124005 137.89962 0 0 -1.106707,1 z m 1.00586,0 h 3.349609 l -0.152344,2.06836 h -3.40625 z m 4.351562,0 h 3.09961 1 3.43164 l 0.152344,2.06836 h -3.583984 -1 -3.251953 z m 8.535157,0 h 3.419921 l 0.210938,2.06836 h -3.480469 z m -13.197266,3.06836 h 3.435547 1.001953 3.324219 v 2.546875 h -3.509766 -1.001953 -3.509766 z m 8.761719,0 h 3.65625 1.003906 3.507812 l 0.257813,2.546875 h -3.580078 -1.003906 -3.841797 z m -9.123047,3.546875 h 3.537109 l -0.193359,2.664062 h -3.615235 z m 4.541015,0 h 3.582032 1 3.914062 l 0.195313,2.664062 h -4.109375 -1 -3.777344 z m 9.5,0 h 3.609375 l 0.271485,2.664062 h -3.685547 z m -14.414062,3.664062 h 3.642578 1.003906 3.84961 v 2.66211 h -4.044922 -1.001953 -3.71875 z m 9.496094,0 h 4.18164 1.003907 3.714843 l 0.269531,2.66211 h -3.791015 -1.001953 -4.376953 z m -9.867188,3.66211 h 3.746094 L 14,27.789062 h -3.820312 z m 4.75,0 h 4.117188 1 4.449218 l 0.185547,2.546874 h -4.634765 -1 -4.302735 z m 10.570313,0 h 3.818359 l 0.257813,2.546874 h -3.890626 z m -15.681641,3.546874 h 3.849609 1.001954 4.376953 v 2.507813 H 14.748047 13.744141 9.8242188 Z m 10.228516,0 h 4.708984 1.001953 3.919922 l 0.255859,2.507813 h -3.992187 -1.003906 -4.890625 z"
inkscape:path-effect="#path-effect206"
inkscape:original-d="m 10.951172,10.300781 -0.101563,1 -0.208984,2.06836 -0.101563,1 -0.259765,2.546875 -0.101563,1 -0.2695309,2.664062 -0.1015625,1 -0.2714844,2.66211 -0.1015624,1 -0.2578126,2.546874 -0.1015624,1 -0.2558594,2.507813 -0.1015625,1 h 1.0058593 3.9492188 1.001953 4.632813 1 4.964843 1.001954 4.021484 1.005859 l -0.101562,-1 -0.25586,-2.507813 -0.101562,-1 -0.257813,-2.546874 -0.101562,-1 -0.271484,-2.66211 -0.101563,-1 -0.269531,-2.664062 -0.101563,-1 -0.259765,-2.546875 -0.101563,-1 -0.210937,-2.06836 -0.101563,-1 h -1.003906 -3.390625 -1.003906 -3.359375 -1 -3.027344 -1.001953 -3.320313 z m 0.904297,1 h 3.349609 l -0.152344,2.06836 h -3.40625 z m 4.351562,0 h 3.09961 1 3.43164 l 0.152344,2.06836 h -3.583984 -1 -3.251953 z m 8.535157,0 h 3.419921 l 0.210938,2.06836 h -3.480469 z m -13.197266,3.06836 h 3.435547 1.001953 3.324219 v 2.546875 h -3.509766 -1.001953 -3.509766 z m 8.761719,0 h 3.65625 1.003906 3.507812 l 0.257813,2.546875 h -3.580078 -1.003906 -3.841797 z m -9.123047,3.546875 h 3.537109 l -0.193359,2.664062 h -3.615235 z m 4.541015,0 h 3.582032 1 3.914062 l 0.195313,2.664062 h -4.109375 -1 -3.777344 z m 9.5,0 h 3.609375 l 0.271485,2.664062 h -3.685547 z m -14.414062,3.664062 h 3.642578 1.003906 3.84961 v 2.66211 h -4.044922 -1.001953 -3.71875 z m 9.496094,0 h 4.18164 1.003907 3.714843 l 0.269531,2.66211 h -3.791015 -1.001953 -4.376953 z m -9.867188,3.66211 h 3.746094 L 14,27.789062 h -3.820312 z m 4.75,0 h 4.117188 1 4.449218 l 0.185547,2.546874 h -4.634765 -1 -4.302735 z m 10.570313,0 h 3.818359 l 0.257813,2.546874 h -3.890626 z m -15.681641,3.546874 h 3.849609 1.001954 4.376953 v 2.507813 H 14.748047 13.744141 9.8242188 Z m 10.228516,0 h 4.708984 1.001953 3.919922 l 0.255859,2.507813 h -3.992187 -1.003906 -4.890625 z"
transform="translate(-0.03578843,-1.298844)" />
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,242 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_frame.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="16"
inkscape:cx="11.6875"
inkscape:cy="6.28125"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><g
id="g5-0"
transform="translate(6.4998844,6.0000382)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2"
style="fill:#4e2ca6;fill-opacity:1;fill-rule:nonzero" /></g><g
id="g5-0-8"
transform="matrix(-1,0,0,1,33.44613,6.0000382)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1-3"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2-5"
style="fill:#ff2323;fill-opacity:1" /></g><g
id="g5-0-8-2"
transform="rotate(180,16.723064,16.999981)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1-3-0"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2-5-1"
style="fill:#ff2323;fill-opacity:1" /></g><g
id="g5-0-8-2-4"
transform="matrix(1,0,0,-1,6.4998854,33.999963)"
style="fill:#ff2323;fill-opacity:1"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4-1-3-0-0"
style="fill:#ff2323;fill-opacity:1" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5-2-5-1-6"
style="fill:#ff2323;fill-opacity:1" /></g><path
id="path13"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.407813 A 0.490625,0.490625 45 0 0 11.463281,17.5 h 0.0094 a 0.4999428,0.4999428 135 0 0 0.499943,-0.499943 V 15.601562 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.255468 a 0.4984375,0.4984375 135 0 0 0.498438,-0.498437 v -0.0018 A 0.4997516,0.4997516 45 0 0 17.472904,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect15" /><path
id="path16"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.439063 A 0.459375,0.459375 45 0 0 11.432031,17.5 h 0.04066 a 0.499968,0.499968 135 0 0 0.499968,-0.499968 l 0,-1.39847 V 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.271094 a 0.4828125,0.4828125 135 0 0 0.482812,-0.482812 V 11.00584 A 0.50584,0.50584 45 0 0 17.466816,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect17"
transform="matrix(-1,0,0,1,39.939926,2.52e-5)" /><path
id="path17"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.439063 A 0.459375,0.459375 45 0 0 11.432031,17.5 h 0.04066 a 0.499968,0.499968 135 0 0 0.499968,-0.499968 l 0,-1.39847 V 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.271094 a 0.4828125,0.4828125 135 0 0 0.482812,-0.482812 V 11.00584 A 0.50584,0.50584 45 0 0 17.466816,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect19"
transform="rotate(180,19.972885,20.000096)" /><path
id="path19"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="m 10.972656,11.5 v 4.101562 1.439063 A 0.459375,0.459375 45 0 0 11.432031,17.5 h 0.04066 a 0.499968,0.499968 135 0 0 0.499968,-0.499968 l 0,-1.39847 V 12.000236 A 0.5002363,0.5002363 135 0 1 12.472892,11.5 h 0.241952 0.503906 4.271094 a 0.4828125,0.4828125 135 0 0 0.482812,-0.482812 V 11.00584 A 0.50584,0.50584 45 0 0 17.466816,10.5 H 13.21875 12.714844 11.972656 a 1,1 135 0 0 -1,1 z"
inkscape:original-d="m 10.972656,10.5 v 1 4.101562 V 17.5 h 1 V 15.601562 11.5 h 0.742188 0.503906 4.753906 v -1 H 13.21875 12.714844 11.972656 Z"
inkscape:path-effect="#path-effect21"
transform="matrix(1,0,0,-1,0.0060898,39.99995)" /></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,387 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_join.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="18.3125"
inkscape:cy="22.328125"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
id="rect35"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(0.294922,2.953125)" /><path
id="rect35-3"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35-7"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(9.345703,2.953125)" /></svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,150 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_real_preview.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect229"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,0.49844035,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.49844035,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect206"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0051443,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0058593,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0051442,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.003906,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect121"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.97194886,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.9540633,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.94238026,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="22.627417"
inkscape:cx="25.279067"
inkscape:cy="19.202368"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path227"
style="fill:#009688;fill-opacity:1;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="m 9.0234375,12.297268 v 0.499607 0.002 0.002 V 28.5 v 1 h 1.0000005 19.925781 1 v -1 -14.027344 -1.673828 -0.516008 c 0,-0.267301 -0.216678,-0.487988 -0.483974,-0.487257 -0.01082,3e-5 -0.02151,6.6e-5 -0.03209,1.08e-4 -0.267272,0.0011 -0.483932,0.219848 -0.483932,0.487149 v 0.516008 1.359375 c -0.414107,0.519735 -1.042494,0.822425 -1.707031,0.822266 -1.124527,-0.0023 -2.063124,-0.858885 -2.167969,-1.978516 -0.0077,-0.06746 -0.01228,-0.135241 -0.01367,-0.203125 1.13e-4,-0.08617 0.0053,-0.172259 0.01563,-0.257812 -0.0105,-0.134093 -0.02949,-0.267265 -0.0568,-0.398724 -0.03994,-0.192221 -0.250872,-0.343464 -0.4473,-0.343464 -0.192979,0 -0.401264,0.151193 -0.44139,0.343374 -0.02852,0.136589 -0.04805,0.275032 -0.05841,0.414439 0.0097,0.08038 0.01491,0.161231 0.01563,0.242187 -0.001,0.0718 -0.0056,0.143498 -0.01367,0.214844 -0.110484,1.114992 -1.047518,1.965076 -2.167969,1.966797 -1.204815,-1.73e-4 -2.181468,-0.976826 -2.181641,-2.181641 -1.87e-4,-0.221094 -0.02342,-0.441241 -0.06912,-0.656906 -0.04069,-0.192058 -0.25223,-0.343094 -0.448658,-0.343094 -0.177877,0 -0.374076,0.151139 -0.414397,0.34328 -0.04525,0.215649 -0.06805,0.435732 -0.06783,0.65672 -1.73e-4,1.204815 -0.976825,2.181468 -2.18164,2.181641 -1.120451,-0.0017 -2.057485,-0.851805 -2.167969,-1.966797 -0.0081,-0.07135 -0.01265,-0.143048 -0.01367,-0.214844 1.13e-4,-0.08617 0.0053,-0.172259 0.01563,-0.257812 -0.01085,-0.134196 -0.03019,-0.267448 -0.05787,-0.398962 -0.04043,-0.192117 -0.251758,-0.343226 -0.448186,-0.343226 -0.192979,0 -0.400882,0.151323 -0.440519,0.343606 -0.02815,0.136537 -0.04732,0.274902 -0.05733,0.414207 0.0097,0.08038 0.01491,0.161231 0.01563,0.242187 -0.0014,0.06788 -0.006,0.135666 -0.01367,0.203125 -0.104845,1.119631 -1.043442,1.976207 -2.167969,1.978516 -1.205578,9.06e-4 -2.18342,-0.976063 -2.183593,-2.181641 v -0.50156 a 0.49844035,0.49844035 45 0 0 -0.4984403,-0.49844 h -0.00315 a 0.49844035,0.49844035 135 0 0 -0.4984403,0.49844 z m 11.2011715,2.204685 c 0.582376,0.918834 1.593793,1.476477 2.681641,1.478516 1.079194,-0.0024 2.083602,-0.551691 2.667969,-1.458985 0.584367,0.907294 1.588775,1.456557 2.667969,1.458985 0.604585,1.64e-4 1.196705,-0.171917 1.707031,-0.496094 V 28.5 H 10.023438 V 15.115234 c 0.590891,0.556323 1.372022,0.865842 2.183593,0.865235 1.079194,-0.0024 2.083602,-0.551691 2.667969,-1.458985 0.584367,0.907294 1.588775,1.456557 2.667969,1.458985 1.087848,-0.002 2.099265,-0.559682 2.68164,-1.478516 z"
sodipodi:nodetypes="ccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
inkscape:path-effect="#path-effect229"
inkscape:original-d="m 9.0234375,11.798828 v 0.998047 0.002 0.002 V 28.5 v 1 h 1.0000005 19.925781 1 v -1 -14.027344 -1.673828 -1 c -0.473966,-0.0071 -0.666658,-8.7e-4 -1,0 v 1 1.359375 c -0.414107,0.519735 -1.042494,0.822425 -1.707031,0.822266 -1.124527,-0.0023 -2.063124,-0.858885 -2.167969,-1.978516 -0.0077,-0.06746 -0.01228,-0.135241 -0.01367,-0.203125 1.13e-4,-0.08617 0.0053,-0.172259 0.01563,-0.257812 -0.01977,-0.252421 -0.06961,-0.501579 -0.148438,-0.742188 h -0.705078 c -0.08072,0.245558 -0.131226,0.500038 -0.15039,0.757813 0.0097,0.08038 0.01491,0.161231 0.01563,0.242187 -0.001,0.0718 -0.0056,0.143498 -0.01367,0.214844 -0.110484,1.114992 -1.047518,1.965076 -2.167969,1.966797 -1.204815,-1.73e-4 -2.181468,-0.976826 -2.181641,-2.181641 -2.87e-4,-0.339851 -0.05502,-0.677463 -0.162109,-1 H 19.88478 c -0.106429,0.322646 -0.1605,0.660254 -0.160157,1 -1.73e-4,1.204815 -0.976825,2.181468 -2.18164,2.181641 -1.120451,-0.0017 -2.057485,-0.851805 -2.167969,-1.966797 -0.0081,-0.07135 -0.01265,-0.143048 -0.01367,-0.214844 1.13e-4,-0.08617 0.0053,-0.172259 0.01563,-0.257812 -0.02041,-0.2525 -0.0709,-0.50166 -0.150391,-0.742188 h -0.705078 c -0.08006,0.245641 -0.129907,0.500119 -0.148437,0.757813 0.0097,0.08038 0.01491,0.161231 0.01563,0.242187 -0.0014,0.06788 -0.006,0.135666 -0.01367,0.203125 -0.104845,1.119631 -1.043442,1.976207 -2.167969,1.978516 -1.205578,9.06e-4 -2.18342,-0.976063 -2.183593,-2.181641 v -1 z m 11.2011715,2.703125 c 0.582376,0.918834 1.593793,1.476477 2.681641,1.478516 1.079194,-0.0024 2.083602,-0.551691 2.667969,-1.458985 0.584367,0.907294 1.588775,1.456557 2.667969,1.458985 0.604585,1.64e-4 1.196705,-0.171917 1.707031,-0.496094 V 28.5 H 10.023438 V 15.115234 c 0.590891,0.556323 1.372022,0.865842 2.183593,0.865235 1.079194,-0.0024 2.083602,-0.551691 2.667969,-1.458985 0.584367,0.907294 1.588775,1.456557 2.667969,1.458985 1.087848,-0.002 2.099265,-0.559682 2.68164,-1.478516 z"
transform="translate(0.26516504,0.08838835)" />
</svg>

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -0,0 +1,260 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_snap.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="45.254834"
inkscape:cx="14.363106"
inkscape:cy="12.827359"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
d="M 20.422985,9.2501548 V 29.968905 H 28.78236 A 1.015625,1.015625 135 0 0 29.797985,28.95328 V 9.2501548 A 1.015625,1.015625 45 0 0 28.78236,8.2345298 h -7.34375 a 1.015625,1.015625 135 0 0 -1.015625,1.015625 z m 1.015625,-0.015625 h 7.34375 V 28.968905 h -7.34375 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path23"
inkscape:path-effect="#path-effect24"
inkscape:original-d="M 20.422985,8.2345298 V 29.968905 h 9.375 V 8.2345298 Z m 1.015625,1 h 7.34375 V 28.968905 h -7.34375 z" /><path
d="m 11.047985,11.324808 v 19.616307 a 1.059034,1.059034 45 0 0 1.059034,1.059034 h 7.256932 a 1.059034,1.059034 135 0 0 1.059034,-1.059034 V 10.265774 h -8.315966 a 1.059034,1.059034 135 0 0 -1.059034,1.059034 z m 1.015625,-0.05903 h 7.34375 v 19.734375 h -7.34375 z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path23-8"
inkscape:path-effect="#path-effect25"
inkscape:original-d="m 11.047985,10.265774 v 21.734375 h 9.375 V 10.265774 Z m 1.015625,1 h 7.34375 v 19.734375 h -7.34375 z" /><path
id="use25"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
d="M 21.439453 8.234375 A 1.015625 1.015625 0 0 0 20.423828 9.25 L 20.423828 10.265625 L 12.107422 10.265625 A 1.059034 1.059034 0 0 0 11.048828 11.324219 L 11.048828 30.941406 A 1.059034 1.059034 0 0 0 12.107422 32 L 19.363281 32 A 1.059034 1.059034 0 0 0 20.423828 30.941406 L 20.423828 29.96875 L 28.783203 29.96875 A 1.015625 1.015625 0 0 0 29.798828 28.953125 L 29.798828 9.25 A 1.015625 1.015625 0 0 0 28.783203 8.234375 L 21.439453 8.234375 z M 21.439453 9.234375 L 28.783203 9.234375 L 28.783203 28.96875 L 21.439453 28.96875 L 21.439453 9.234375 z M 12.064453 11.265625 L 19.408203 11.265625 L 19.408203 31 L 12.064453 31 L 12.064453 11.265625 z " /></svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,387 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_unjoin.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="18.3125"
inkscape:cy="22.328125"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
id="rect35"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(-1.142612,2.9530855)" /><path
id="rect35-3"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.74375,1 h 7.563281 a 0.24375,0.24375 45 0 1 0.24375,0.24375 v 7.575 a 0.24375,0.24375 135 0 1 -0.24375,0.24375 h -7.563281 a 0.24375,0.24375 45 0 1 -0.24375,-0.24375 v -7.575 a 0.24375,0.24375 135 0 1 0.24375,-0.24375 z"
inkscape:path-effect="#path-effect35-7"
inkscape:original-d="m 11.154297,12.015625 c -0.277007,0 -0.5,0.222993 -0.5,0.5 v 9.050781 c 0,0.0019 -2.2e-5,0.0039 0,0.0059 -2.3e-5,0.002 0,0.0039 0,0.0059 3.45e-4,0.01513 0.0023,0.03016 0.0039,0.04492 0.02282,0.255546 0.234414,0.455078 0.496094,0.455078 h 9.050781 c 0.277007,0 0.5,-0.222993 0.5,-0.5 v -0.002 -9.048828 c 9e-5,-0.0039 0,-0.0078 0,-0.01172 0,-0.277007 -0.222993,-0.5 -0.5,-0.5 z m 0.5,1 h 8.050781 v 8.0625 h -8.050781 z"
transform="translate(10.783203,2.9530855)" /></svg>

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,383 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_uv_select_edge.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="13.65625"
inkscape:cy="21.984375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><rect
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
id="rect35"
width="1.0000261"
height="23.343758"
x="19.972986"
y="8.3281212"
rx="0.50001299"
ry="0.50001299" /></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,413 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_uv_select_island.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect37"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.22141448,0,1 @ F,0,1,1,0,0.22141448,0,1 @ F,0,1,1,0,0.22141448,0,1 @ F,0,0,1,0,0.22141448,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect36"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="13.65625"
inkscape:cy="21.984375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
id="rect35"
style="fill:#009688;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1;stroke-dasharray:none;stroke-opacity:1;paint-order:stroke fill markers"
d="m 9.5039062,8.65625 c -0.2770071,0 -0.5,0.2229928 -0.5,0.5 V 31.484375 31.5 c 3.682e-4,0.0119 7.748e-4,0.02348 0.00195,0.03516 C 9.0240314,31.795393 9.2388928,32 9.5039062,32 c 0.043378,0 0.08492,-0.0053 0.125,-0.01563 H 31.722656 c 0.04008,0.01029 0.08162,0.01563 0.125,0.01563 0.265014,0 0.479875,-0.204607 0.498047,-0.464844 0.0017,-0.01685 0.002,-0.03347 0.002,-0.05078 V 9.15625 c 0,-0.2770072 -0.222993,-0.5 -0.5,-0.5 z m 0.7214138,1 20.900922,0 a 0.22141448,0.22141448 45 0 1 0.221414,0.2214145 l 0,20.8852965 a 0.22141448,0.22141448 135 0 1 -0.221414,0.221414 l -20.900922,0 a 0.22141448,0.22141448 45 0 1 -0.221414,-0.221414 l 0,-20.8852965 A 0.22141448,0.22141448 135 0 1 10.22532,9.65625 Z"
inkscape:path-effect="#path-effect37"
inkscape:original-d="m 9.5039062,8.65625 c -0.2770071,0 -0.5,0.2229928 -0.5,0.5 V 31.484375 31.5 c 3.682e-4,0.0119 7.748e-4,0.02348 0.00195,0.03516 C 9.0240314,31.795393 9.2388928,32 9.5039062,32 c 0.043378,0 0.08492,-0.0053 0.125,-0.01563 H 31.722656 c 0.04008,0.01029 0.08162,0.01563 0.125,0.01563 0.265014,0 0.479875,-0.204607 0.498047,-0.464844 0.0017,-0.01685 0.002,-0.03347 0.002,-0.05078 V 9.15625 c 0,-0.2770072 -0.222993,-0.5 -0.5,-0.5 z m 0.4999998,1 h 21.34375 v 21.328125 h -21.34375 z"
transform="translate(-0.5,-0.32812498)" /></svg>

After

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -0,0 +1,378 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_uv_select_vertex.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xml:space="preserve"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs17"><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect34"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 @ F,0,0,1,0,0.965625,0,1 | F,0,0,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1 @ F,0,1,1,0,0.3257813,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect33"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect30"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.2615305,0,1 @ F,0,0,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 @ F,0,1,1,0,1.2615305,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect29"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect27"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect25"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,1.059034,0,1 @ F,0,1,1,0,1.059034,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect24"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,0,0,1 @ F,0,1,1,0,1.015625,0,1 @ F,0,0,1,0,1.015625,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect23"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect21"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect19"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect17"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.459375,0,1 @ F,0,0,1,0,0.499968,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.4828125,0,1 @ F,0,0,1,0,0.50584,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect15"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.5002363,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><linearGradient
id="swatch7"
inkscape:swatch="solid"><stop
style="stop-color:#77eaa3;stop-opacity:1;"
offset="0"
id="stop7" /></linearGradient><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect3"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.49883385,0,1 @ F,0,0,1,0,0.48672135,0,1 @ F,0,0,1,0,0.5261776,0,1 @ F,0,0,1,0,0.50001454,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect2"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect31-9-1"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 @ F,0,0,1,0,1.2263883,0,1 | F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1 @ F,0,1,1,0,0.47508735,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /><inkscape:path-effect
effect="fillet_chamfer"
id="path-effect35-7"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,1,1,0,0.24375,0,1 @ F,0,0,1,0,0.24375,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" /></defs><sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="16.6875"
inkscape:cy="29.109375"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" /><path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" /><path
d="m 35.473026,33.999994 v -0.999987 a 0.500013,0.500013 0 0 0 -1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 1 -0.499993,0.499994 h -5.000013 a 0.5000126,0.5000126 0 0 0 0,1.000025 h 5.000013 a 1.5,1.5 0 0 0 1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7" /><path
d="m 5.9730079,35.500013 h 0.999987 a 0.500013,0.500013 0 0 0 0,-1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 1 -0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 0 -1.000025,0 v 5.000013 a 1.5,1.5 0 0 0 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3" /><path
d="m 4.4729879,33.999994 v -0.999987 a 0.500013,0.500013 0 0 1 1.000026,0 v 0.999987 a 0.49999998,0.49999998 0 0 0 0.499993,0.499994 H 10.97302 a 0.5000126,0.5000126 0 0 1 0,1.000025 H 5.9730069 a 1.5,1.5 0 0 1 -1.500019,-1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5" /><path
d="m 5.9730079,4.499987 h 0.999987 a 0.50001302,0.50001302 0 0 1 0,1.000026 h -0.999987 a 0.49999998,0.49999998 0 0 0 -0.499994,0.499993 v 5.000013 a 0.5000126,0.5000126 0 0 1 -1.0000251,0 V 6.000006 A 1.5,1.5 0 0 1 5.9730079,4.499987 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9" /><path
d="m 4.4729878,6.000006 v 0.999987 a 0.50001302,0.50001302 0 0 0 1.0000261,0 V 6.000006 A 0.49999998,0.49999998 0 0 1 5.9730068,5.500012 H 10.97302 a 0.5000126,0.5000126 0 0 0 0,-1.000025 H 5.9730068 a 1.5,1.5 0 0 0 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8" /><path
d="m 33.973006,4.499986 h -0.999987 a 0.50001302,0.50001302 0 0 0 0,1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 1 34.473,6.000005 v 5.000013 a 0.5000126,0.5000126 0 0 0 1.000025,0 V 6.000005 A 1.5,1.5 0 0 0 33.973006,4.499986 Z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-3-9-8" /><path
d="m 35.473026,6.000005 v 0.999987 a 0.50001305,0.50001305 0 0 1 -1.000026,0 V 6.000005 A 0.49999998,0.49999998 0 0 0 33.973007,5.500011 h -5.000013 a 0.5000126,0.5000126 0 0 1 0,-1.000025 h 5.000013 a 1.5,1.5 0 0 1 1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6-7-5-8-2" /><g
id="g5"><use
x="0"
y="0"
xlink:href="#path3-6-3-9"
id="use4" /><use
x="0"
y="0"
xlink:href="#path3-6-7-5-8"
id="use5" /></g><path
d="M 20.472999,14.75 A 5.2386374,5.25 0 0 0 15.234718,20 5.2386374,5.25 0 0 0 20.472999,25.25 5.2386374,5.25 0 0 0 25.71128,20 5.2386374,5.25 0 0 0 20.472999,14.75 Z m 0,0.974609 A 4.265625,4.2748771 0 0 1 24.738624,20 4.265625,4.2748771 0 0 1 20.472999,24.275391 4.265625,4.2748771 0 0 1 16.207374,20 4.265625,4.2748771 0 0 1 20.472999,15.724609 Z"
style="fill:#009688;stroke-width:0.1;paint-order:stroke fill markers"
id="path39" /></svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -0,0 +1,163 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="texture_displacement_wireframe.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect294"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.636718,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect229"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,1,1,0,0.49844035,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0.48399193,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0.35566405,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.49844035,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect206"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0051443,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0058593,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0051442,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.003906,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect121"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.97194886,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.9540633,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0.94238026,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="26.09375"
inkscape:cy="31.421875"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path291"
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="M 8.8085938 7.4296875 A 1 1 0 0 0 7.8085938 8.4296875 L 7.8085938 19.261719 L 7.8085938 20.261719 L 7.8085938 31.09375 A 1 1 0 0 0 8.8085938 32.09375 L 18.796875 32.09375 L 19.640625 32.09375 L 20.210938 32.09375 L 20.640625 32.09375 L 31.472656 32.09375 A 1 1 0 0 0 32.472656 31.09375 L 32.472656 20.261719 L 32.472656 19.830078 L 32.472656 19.261719 L 32.472656 18.416016 L 32.472656 8.4296875 L 32.472656 8.2070312 L 32.472656 8.0664062 A 0.636718 0.636718 0 0 0 31.835938 7.4296875 L 31.472656 7.4296875 L 20.640625 7.4296875 L 19.640625 7.4296875 L 8.8085938 7.4296875 z M 20.640625 8.4296875 L 30.835938 8.4296875 L 20.640625 18.626953 L 20.640625 8.4296875 z M 31.472656 9.2070312 L 31.472656 19.261719 L 21.419922 19.261719 L 31.472656 9.2070312 z M 20.640625 20.261719 L 30.626953 20.261719 L 20.640625 30.25 L 20.640625 20.261719 z M 31.472656 20.830078 L 31.472656 31.09375 L 21.210938 31.09375 L 31.472656 20.830078 z " />
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="toolbar_big_brush.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17" />
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="5.6568543"
inkscape:cx="-13.081475"
inkscape:cy="98.994949"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<path
id="path30"
style="fill:#009688;fill-opacity:1;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers"
d="M 30.109375 6.9648438 A 1.8710937 1.8710937 0 0 0 28.705078 7.5996094 A 1.8710937 1.8710937 0 0 0 28.591797 7.7421875 L 18.103516 20.615234 A 6.1070809 6.1070809 0 0 0 17.226562 20.552734 A 6.1070809 6.1070809 0 0 0 11.513672 24.503906 A 6.1070809 6.1070809 0 0 0 11.121094 26.658203 A 6.1070809 6.1070809 0 0 0 11.121094 26.757812 A 4.4931102 4.4931102 0 0 1 11.15625 27.3125 A 4.4931102 4.4931102 0 0 1 7.7890625 31.662109 A 4.4931102 4.4931102 0 0 1 7.5839844 31.708984 A 0.55609465 0.55242634 0 0 0 7.2558594 32.212891 A 0.55609465 0.55242634 0 0 0 7.8105469 32.765625 A 0.55609465 0.55242634 0 0 0 7.8125 32.765625 L 7.9570312 32.765625 L 17.224609 32.765625 L 17.226562 32.765625 A 6.1070809 6.1070809 0 0 0 23.333984 26.658203 A 6.1070809 6.1070809 0 0 0 22.792969 24.144531 L 31.666016 9.875 A 1.8710937 1.8710937 0 0 1 31.638672 9.9101562 A 1.8710937 1.8710937 0 0 0 31.699219 9.8242188 A 1.8710937 1.8710937 0 0 0 31.980469 8.8359375 A 1.8710937 1.8710937 0 0 0 31.724609 7.890625 A 1.8710937 1.8710937 0 0 0 30.480469 7.0019531 A 1.8710937 1.8710937 0 0 0 30.109375 6.9648438 z M 28.525391 7.8476562 A 1.8710937 1.8710937 0 0 0 28.472656 7.9335938 A 1.8710937 1.8710937 0 0 1 28.478516 7.9199219 A 1.8710937 1.8710937 0 0 1 28.525391 7.8476562 z M 30.109375 7.9570312 A 0.87817276 0.87817276 0 0 1 30.988281 8.8359375 A 0.87817276 0.87817276 0 0 1 30.90625 9.2050781 A 0.87817276 0.87817276 0 0 1 30.791016 9.390625 L 29.974609 10.703125 L 22.230469 23.15625 A 6.1070809 6.1070809 0 0 0 19.183594 20.873047 L 28.388672 9.5703125 L 29.359375 8.3808594 A 0.87817276 0.87817276 0 0 1 29.511719 8.1933594 A 0.87817276 0.87817276 0 0 1 30.109375 7.9570312 z M 28.457031 7.9628906 A 1.8710937 1.8710937 0 0 0 28.373047 8.1425781 A 1.8710937 1.8710937 0 0 1 28.457031 7.9628906 z M 28.300781 8.359375 A 1.8710937 1.8710937 0 0 0 28.263672 8.5507812 A 1.8710937 1.8710937 0 0 1 28.298828 8.3613281 A 1.8710937 1.8710937 0 0 1 28.300781 8.359375 z M 17.226562 21.65625 A 5.0022264 5.0022264 0 0 1 22.230469 26.658203 A 5.0022264 5.0022264 0 0 1 17.226562 31.662109 L 13.724609 31.662109 L 10.199219 31.662109 A 5.6057596 5.6057596 0 0 0 11.822266 29.501953 A 5.6057596 5.6057596 0 0 0 12.267578 27.3125 A 5.0022264 5.0022264 0 0 1 12.224609 26.658203 A 5.0022264 5.0022264 0 0 1 12.224609 26.621094 A 5.0022264 5.0022264 0 0 1 17.226562 21.65625 z " />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
</svg>

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

@@ -0,0 +1,97 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="toolbar_face.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect48"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,1.0055868,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.4911902,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,1.0376412,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 | F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect47"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
radius="0"
unit="px"
method="auto"
mode="F"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="22.627417"
inkscape:cx="23.113553"
inkscape:cy="18.318485"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<path
d="m 33.973006,35.500013 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 A 0.49999998,0.49999998 0 0 0 34.473,33.999994 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.973012,35.500013 H 5.9730109 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 6.000006 a 1.5,1.5 0 0 1 1.5,-1.500019 H 33.973006 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 6.000006 A 0.49999998,0.49999998 0 0 0 33.973006,5.500012 H 5.9730109 a 0.49999998,0.49999998 0 0 0 -0.499997,0.499994 v 27.999988 a 0.49999998,0.49999998 0 0 0 0.499997,0.499993 H 19.973012 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999;fill-opacity:1"
id="path4-6" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect40"
width="15.363631"
height="1.000026"
x="18.609375"
y="34.499989" />
<rect
style="fill:#b6b6b6;fill-opacity:1;stroke:none;stroke-width:0;stroke-linejoin:miter;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0;paint-order:stroke fill markers"
id="rect41"
width="1.000025"
height="11.921875"
x="34.473"
y="18.546875" />
<path
id="path42"
style="fill:#009688;stroke-width:0;stroke-opacity:0;paint-order:stroke fill markers;fill-opacity:1"
d="M 19.58431,7.9401469 19.519531,8.0820312 9.1699219,30.736328 a 0.61329334,0.61329334 42.827026 0 0 0.828125,0.767578 l 0,0 20.7832031,-7.320312 0.0019,-6.61e-4 a 0.7874119,0.7874119 107.77086 0 0 0.382891,-1.194652 L 20.714844,8.0449219 20.578195,7.8494958 A 0.57476577,0.57476577 174.78854 0 0 19.58431,7.9401469 Z M 20.232422,9.1015625 30.1875,23.332031 10.576172,30.240234 Z"
inkscape:path-effect="#path-effect48"
inkscape:original-d="M 20.001953,7.0253906 19.519531,8.0820312 9.1699219,30.736328 8.5917969,32 9.9980469,31.503906 30.78125,24.183594 31.761719,23.837891 31.166016,22.988281 20.714844,8.0449219 Z M 20.232422,9.1015625 30.1875,23.332031 10.576172,30.240234 Z" />
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@@ -0,0 +1,240 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="40"
height="40"
viewBox="0 0 40 40"
version="1.1"
id="svg17"
sodipodi:docname="toolbar_texture_displacement.svg"
inkscape:version="1.4.2 (f4327f4, 2025-05-13)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<defs
id="defs17" />
<sodipodi:namedview
id="namedview17"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="32"
inkscape:cx="27.578125"
inkscape:cy="27.671875"
inkscape:window-width="3840"
inkscape:window-height="2126"
inkscape:window-x="-11"
inkscape:window-y="-11"
inkscape:window-maximized="1"
inkscape:current-layer="svg17" />
<g
id="g17"
transform="translate(0.69176086,0.249987)">
<path
d="m 27.659236,35.176022 h -4.399987 c -0.508996,0 -0.844996,0 -0.954999,-0.394015 a 0.59299998,0.59299998 0 0 1 0.36,-0.669997 2.3,2.3 0 0 0 1.2,-2.188989 3.253,3.253 0 1 1 3.800012,3.205984 z M 24.12125,34.175997 h 2.988 a 2.253,2.253 0 1 0 -2.252999,-2.252976 3.445,3.445 0 0 1 -0.735001,2.252976 z"
style="fill:#009688;stroke-width:0.999999"
id="path1-6" />
<path
d="m 30.281245,30.538996 -0.846009,-0.534993 4.364031,-6.899981 A 0.222,0.222 0 0 0 33.439456,22.84588 l -5.4,6.644032 -0.776013,-0.63103 5.400001,-6.643994 a 1.222,1.222 0 0 1 1.981984,1.422992 z"
style="fill:#009688;stroke-width:0.999999"
id="path2-0" />
<path
d="m 33.281245,35.250026 h -0.999987 a 0.500013,0.500013 0 0 1 0,-1.000026 h 0.999987 a 0.49999998,0.49999998 0 0 0 0.499994,-0.499993 v -5.000013 a 0.5000126,0.5000126 0 0 1 1.000025,0 v 5.000013 a 1.5,1.5 0 0 1 -1.500019,1.500019 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path3-6" />
<path
d="M 19.281251,35.250026 H 5.28125 a 1.5,1.5 0 0 1 -1.5,-1.500019 V 5.750019 A 1.5,1.5 0 0 1 5.28125,4.25 h 27.999995 a 1.5,1.5 0 0 1 1.500019,1.500019 v 12.999987 a 0.5000126,0.5000126 0 0 1 -1.000025,0 V 5.750019 A 0.49999998,0.49999998 0 0 0 33.281245,5.250025 H 5.28125 A 0.49999998,0.49999998 0 0 0 4.781253,5.750019 V 33.750007 A 0.49999998,0.49999998 0 0 0 5.28125,34.25 h 14.000001 a 0.500013,0.500013 0 0 1 0,1.000026 z"
style="fill:#b6b6b6;stroke-width:0.999999"
id="path4-6" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-1"
width="39.379108"
height="0.99609375"
x="-19.909107"
y="27.094706"
ry="0.49804688"
transform="rotate(-45)" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2"
width="27.566595"
height="0.99609375"
x="7.9031763"
y="-0.27849278"
ry="0.49487707"
transform="rotate(45)" />
<g
id="g7"
transform="matrix(3.7795276,0,0,3.7795276,-350.69182,-427.25001)">
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2"
width="2.8453603"
height="0.2635498"
x="150.08304"
y="13.289762"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2"
width="1.7498204"
height="0.2635498"
x="12.763216"
y="-149.44461"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2-2"
width="0.82876241"
height="0.27337581"
x="13.654455"
y="-148.49495"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6"
width="2.5927844"
height="0.25224927"
x="150.38367"
y="12.286384"
ry="0.12612464"
transform="rotate(45)"
rx="0.12612464" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6-1"
width="1.9647908"
height="0.2635498"
x="151.01166"
y="11.21043"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
</g>
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-8"
width="10.754118"
height="0.99609375"
x="-10.211728"
y="-31.661213"
ry="0.49804688"
transform="rotate(135)"
rx="0.49804688" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-2-9"
width="6.6134944"
height="0.99609375"
x="-33.794979"
y="12.679931"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-2-2-3"
width="3.1323304"
height="1.0332314"
x="-30.282875"
y="16.213961"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
<g
id="g7-1"
transform="matrix(0,-3.7795276,3.7795276,0,-427.55829,389.70233)">
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-9"
width="2.8453603"
height="0.2635498"
x="150.08304"
y="13.289762"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2-8"
width="1.7498204"
height="0.2635498"
x="12.763216"
y="-149.44461"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-2-2-6"
width="0.82876241"
height="0.27337581"
x="13.654455"
y="-148.49495"
ry="0.1317749"
transform="rotate(135)"
rx="0.1317749" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6-5"
width="2.5927844"
height="0.25224927"
x="150.38367"
y="12.286384"
ry="0.12612464"
transform="rotate(45)"
rx="0.12612464" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:0.264999;stroke-dasharray:none"
id="rect6-2-2-6-1-0"
width="1.9647908"
height="0.2635498"
x="151.01166"
y="11.21043"
ry="0.1317749"
transform="rotate(45)"
rx="0.1317749" />
</g>
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-9-4"
width="9.0822706"
height="0.99609375"
x="-36.379971"
y="-4.5927153"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-6-5-5"
width="9.7995005"
height="0.95338309"
x="-36.915619"
y="-8.385005"
ry="0.47669154"
transform="rotate(-135)"
rx="0.47669154" />
<rect
style="fill:#009688;fill-opacity:1;stroke-width:1.00157;stroke-dasharray:none"
id="rect6-2-2-6-1-0-0"
width="7.425981"
height="0.99609375"
x="-34.54211"
y="-12.451604"
ry="0.49804688"
transform="rotate(-135)"
rx="0.49804688" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@@ -0,0 +1,254 @@
#version 110
// See resources/shaders/140/texture_displacement_bump.fs for full documentation; this is the
// GLSL 1.10 compatibility variant (same logic, older syntax).
#define INTENSITY_CORRECTION 0.6
#define PARALLAX_STEPS 24
#define H_AT(uv) texture2D(height_tex, uv).r
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform vec4 uniform_color;
// The printable palette, in **CIELAB** as well as RGB, and how many entries are real. Lab because the
// match has to be perceptual - the same reason the CPU side uses CIEDE2000 - and converting the
// palette once on the CPU is what lets the fragment shader match with a plain squared distance.
// Count 0 means nothing is colouring, and every fragment falls back to uniform_color as before.
uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
uniform mat3 view_normal_matrix;
uniform sampler2D height_tex;
uniform vec2 height_tex_texel;
uniform float depth_mm;
uniform float tiling_scale;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in this volume's local space, for the view ray
uniform bool use_vertex_uv;
// 2x3 affine (lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the dragged island's uv; see the
// 140 variant. Identity when nothing is dragged.
uniform vec4 island_delta_lin;
uniform vec2 island_delta_tr;
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float weight;
varying float island_active;
varying vec2 vertex_uv;
void projection_axes(vec3 n, out vec3 t, out vec3 b)
{
vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
t = vec3(0.0, 1.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else { // planar = p.xy
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 1.0, 0.0);
}
}
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
}
// sRGB -> CIELAB, matching slic3r/Utils/ColorSpaceConvert's RGB2Lab so this picks the same entry the
// bake does.
vec3 srgb_to_lab(vec3 c)
{
vec3 v = vec3(c.r > 0.04045 ? pow((c.r + 0.055) / 1.055, 2.4) : c.r / 12.92,
c.g > 0.04045 ? pow((c.g + 0.055) / 1.055, 2.4) : c.g / 12.92,
c.b > 0.04045 ? pow((c.b + 0.055) / 1.055, 2.4) : c.b / 12.92);
vec3 xyz = vec3(dot(v, vec3(0.4124, 0.3576, 0.1805)) / 0.95047,
dot(v, vec3(0.2126, 0.7152, 0.0722)),
dot(v, vec3(0.0193, 0.1192, 0.9505)) / 1.08883);
vec3 f = vec3(xyz.x > 0.008856 ? pow(xyz.x, 1.0 / 3.0) : (7.787 * xyz.x) + 16.0 / 116.0,
xyz.y > 0.008856 ? pow(xyz.y, 1.0 / 3.0) : (7.787 * xyz.y) + 16.0 / 116.0,
xyz.z > 0.008856 ? pow(xyz.z, 1.0 / 3.0) : (7.787 * xyz.z) + 16.0 / 116.0);
return vec3(116.0 * f.y - 16.0, 500.0 * (f.x - f.y), 200.0 * (f.y - f.z));
}
// Nearest printable colour to a sampled one. Quantizing per *fragment* rather than per facet is the
// whole point of this path: it shows the image at the texture's resolution instead of the mesh's,
// which is what you need while choosing a texture and placing it. The Normal view is where the
// facet-resolution truth - what actually bakes - is shown.
//
// Squared distance in Lab (CIE76) rather than the CPU's CIEDE2000: the two agree except on near-ties,
// and CIEDE2000 per fragment across 64 entries is not worth its cost in a preview.
vec3 quantize_to_palette(vec3 rgb)
{
vec3 lab = srgb_to_lab(rgb);
int best = 0;
float bd = 1.0e20;
for (int i = 0; i < 64; ++i) {
if (i >= palette_count)
break;
vec3 d = lab - palette_lab[i];
float d2 = dot(d, d);
if (d2 < bd) {
bd = d2;
best = i;
}
}
return palette_rgb[best];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
vec3 triangle_normal = normalize(cross(dFdx(model_pos.xyz), dFdy(model_pos.xyz)));
if (volume_mirrored)
triangle_normal = -triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
// has to follow it exactly, or the colour would slide off the relief as the camera orbits.
vec2 color_uv = vec2(0.0);
bool have_uv = false;
if (use_vertex_uv) {
// Mikkelsen surface-gradient bump; see the 140 variant for the full rationale. Scale-exact
// for a conformal LSCM map (no global 1/tiling assumption), and gated by the paint weight
// via a multiply so the branch stays uniform (use_vertex_uv is a uniform).
vec2 uv = (island_active > 0.5)
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv;
color_uv = uv;
have_uv = true;
float h = texture2D(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 sigmaS = dFdx(model_pos.xyz);
vec3 sigmaT = dFdy(model_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal);
vec3 R2 = cross(triangle_normal, sigmaS);
float det = dot(sigmaS, R1);
float dHdx = k * dFdx(h);
float dHdy = k * dFdy(h);
if (abs(det) > 1e-12)
triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det);
} else if (weight > 0.0) {
vec3 t, b;
projection_axes(triangle_normal, t, b);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(eye_model_pos - model_pos.xyz);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
// where the surface sits *below* the undisplaced one.
float h_end_a = amp * (0.0 - midlevel);
float h_end_b = amp * (1.0 - midlevel);
float h_hi = max(h_end_a, h_end_b);
float h_lo = min(h_end_a, h_end_b);
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
// quantising to the step size, and so what keeps the step count affordable.
uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0));
break;
}
prev_uv = cur_uv;
prev_gap = gap;
}
}
color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true;
float hL = texture2D(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture2D(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
float hD = texture2D(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
float hU = texture2D(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0);
vec2 intensity = vec2(0.0);
intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE;
vec3 position = (view_model_matrix * model_pos).xyz;
intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS);
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = quantize_to_palette(texture2D(color_tex, color_uv).rgb);
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}

View File

@@ -0,0 +1,34 @@
#version 110
// See resources/shaders/140/texture_displacement_bump.vs for full documentation; this is the
// GLSL 1.10 compatibility variant.
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
uniform vec2 z_range;
uniform vec4 clipping_plane;
attribute vec3 v_position;
attribute vec3 v_normal; // .x = paint weight (0/1); .y = 1 for the dragged island's vertices
attribute vec2 v_tex_coord; // precomputed texture uv, used only when use_vertex_uv is set
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float weight;
varying float island_active;
varying vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
weight = v_normal.x;
island_active = v_normal.y;
vertex_uv = v_tex_coord;
}

View File

@@ -0,0 +1,70 @@
#version 110
// See resources/shaders/140/texture_displacement_uvcheck.fs; GLSL 1.10 compatibility variant.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform mat3 view_normal_matrix;
uniform bool volume_mirrored;
uniform int mode;
uniform float checker_freq;
uniform float tiling_scale;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float distortion;
varying vec2 vertex_uv;
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
}
vec3 heatmap(float t)
{
t = clamp(t, 0.0, 1.0);
return clamp(vec3(1.5 - abs(4.0 * t - 3.0),
1.5 - abs(4.0 * t - 2.0),
1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
vec3 triangle_normal = normalize(cross(dFdx(model_pos.xyz), dFdy(model_pos.xyz)));
if (volume_mirrored)
triangle_normal = -triangle_normal;
vec3 base;
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(model_pos.xyz, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float intensity = INTENSITY_AMBIENT + max(dot(eye_normal, LIGHT_TOP_DIR), 0.0) * LIGHT_TOP_DIFFUSE
+ max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0) * LIGHT_FRONT_DIFFUSE;
gl_FragColor = vec4(base * intensity, 1.0);
}

View File

@@ -0,0 +1,31 @@
#version 110
// See resources/shaders/140/texture_displacement_uvcheck.vs; GLSL 1.10 compatibility variant.
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
uniform vec2 z_range;
uniform vec4 clipping_plane;
attribute vec3 v_position;
attribute vec3 v_normal; // .x = per-vertex uv distortion
attribute vec2 v_tex_coord; // precomputed texture uv, used only when use_vertex_uv is set
varying vec3 clipping_planes_dots;
varying vec4 model_pos;
varying vec4 world_pos;
varying float distortion;
varying vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
distortion = v_normal.x;
vertex_uv = v_tex_coord;
}

View File

@@ -0,0 +1,338 @@
#version 140
// Fast, geometry-free preview of texture displacement: perturbs the *shading* normal from the
// height texture's local gradient (a bump map), faded out by the per-vertex paint weight. The
// true, exact result is what "Bake" produces via libslic3r/TextureDisplacement.cpp on the CPU.
//
// The bake displaces each surface point along its normal by H = +/- depth_mm * (h(uv) - midlevel),
// with uv from the layer's projection. The perturbed normal is the analytic
//
// N' = normalize(N - (dH/da) * T - (dH/db) * B)
//
// over any orthonormal surface tangent pair (T, B), where the two slopes are real mm-per-mm
// derivatives. Two things have to be right for the preview's apparent depth to match the bake's:
// the tangent frame the gradient is expressed in, and the uv->mm scale that turns a texel
// difference into a slope. Getting the scale wrong is a uniform flattening (a raw texel difference
// is dh over one texel step, not over one mm); getting the frame wrong tilts the bump along the
// wrong axes.
//
// Two projection paths:
// * Triplanar (use_vertex_uv = 0): uv and the tangent axes are derived in-shader from the dominant
// normal axis, mirroring libslic3r's project_planar()/apply_uv_transform(), and the slope is
// formed analytically (there is a closed-form uv, so 1 uv unit is exactly tiling_scale mm). This
// path also runs a parallax step before shading, see below.
//
// Parallax. A pure bump map perturbs shading only, so the pattern is welded to the base surface: it
// does not shift as the camera orbits and it does not get any deeper as depth_mm grows, which is
// exactly when the preview stops reading as real geometry. The triplanar path therefore shades at the
// point the *displaced* surface would show at this pixel rather than at the pixel's own base position.
//
// Two cheaper formulations were tried first and both are wrong here, which is worth recording:
// * Solving Q = P + V * (H(Q) / dot(V, n)) by fixed-point iteration. Geometrically exact, but the
// divisor goes to zero edge-on, and an unbounded step is not a small error - the sample lands a
// large fraction of a tile away and the iteration oscillates instead of converging. It reads as a
// *second, flat copy* of the pattern ghosted over the real one. Clamping the step to one tile does
// not help either: a tile-sized shift lands on the neighbouring tile, which is the same pattern.
// * Offset limiting (Welsh): step along the tangential part of V, whose length caps the shift at one
// depth. Stable and cheap, but it understates parallax by exactly the factor that matters - the
// relief still flattens as soon as the camera tilts, which is the complaint it was meant to fix.
//
// So this ray-marches instead (parallax occlusion mapping). A point at ray parameter s, i.e. P + V * s,
// sits at height s * dot(V, n) above the undisplaced surface. The displaced surface lives in a shell
// between the extreme values of amp * (h - midlevel); the march starts at the top of that shell, where
// the ray is outside the surface by construction, and steps inward until the ray height falls below the
// sampled height. That crossing *is* the visible point - no divergence, no ghosting, and parallax stays
// correct at any angle. The hit is interpolated between the last two samples, which is what keeps
// PARALLAX_STEPS low enough to afford. The march is skipped when sweeping the shell would move the
// sample point less than half a texel (the head-on case), so the common view pays almost nothing.
//
// The gradient/shading below is evaluated at the resulting uv, so the relief both slides correctly
// under camera motion and visibly deepens with depth_mm. What it still cannot do is change the
// model's silhouette or cast shadows; for that, switch the View row to Normal.
// * Precomputed uv (use_vertex_uv = 1, used for LSCM): uv comes per-vertex from the CPU (the LSCM
// unwrap with island placement + tiling/rotation/offset already folded in), and the perturbed
// normal is built with Mikkelsen's method -- the surface gradient taken straight from the
// screen-space derivatives of the sampled height and position. This makes no uv->mm scale
// assumption, which matters because an LSCM map is conformal, not isometric: the local mm-per-uv
// varies across the chart, so a single global 1/tiling factor (what an earlier version used) got
// the apparent depth wrong. This path is also what makes the fast preview follow the UV editor:
// move an island and its uv -- hence its bump -- moves with it.
#define INTENSITY_CORRECTION 0.6
#define PARALLAX_STEPS 24
// Explicit LOD: the march samples inside non-uniform control flow, where implicit
// derivatives are undefined.
#define H_AT(uv) textureLod(height_tex, uv, 0.0).r
// normalized values for (-0.6/1.31, 0.6/1.31, 1./1.31)
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SPECULAR (0.125 * INTENSITY_CORRECTION)
#define LIGHT_TOP_SHININESS 20.0
// normalized values for (1./1.43, 0.2/1.43, 1./1.43)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform vec4 uniform_color;
// The printable palette, in **CIELAB** as well as RGB, and how many entries are real. Lab because the
// match has to be perceptual - the same reason the CPU side uses CIEDE2000 - and converting the
// palette once on the CPU is what lets the fragment shader match with a plain squared distance.
// Count 0 means nothing is colouring, and every fragment falls back to uniform_color as before.
uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
uniform mat4 view_model_matrix;
uniform mat3 view_normal_matrix;
uniform sampler2D height_tex;
uniform vec2 height_tex_texel; // (1/width, 1/height) of height_tex
uniform float depth_mm;
uniform float tiling_scale;
// Height map width / height. Scales the v axis so a non-square image keeps its proportions
// instead of being squeezed into a square tile - mirrors libslic3r's apply_uv_transform().
uniform float tex_aspect;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool invert;
uniform float midlevel; // the height that means "don't move"; needed by the parallax step
uniform vec3 eye_model_pos; // camera position in this volume's local space, for the view ray
uniform bool use_vertex_uv; // true: sample at vertex_uv with a derived tangent frame (LSCM)
// A 2x3 affine (columns packed as lin = (m00, m01, m10, m11), tr = (m02, m12)) applied to the uv of
// the island currently being dragged in the UV editor (island_active > 0.5). Identity when nothing is
// dragged, so this whole path is a no-op then. Lets a UV island drag move the bump on the model with
// only a uniform update
uniform vec4 island_delta_lin;
uniform vec2 island_delta_tr;
in vec3 clipping_planes_dots;
in vec4 model_pos;
in vec4 world_pos;
in float weight;
in float island_active;
in vec2 vertex_uv;
out vec4 out_color;
// The two model-space axes the triplanar planar coordinate is read off, per dominant normal
// component - same choice libslic3r's project_planar() makes, so planar.x runs along t, planar.y
// along b.
void projection_axes(vec3 n, out vec3 t, out vec3 b)
{
vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
t = vec3(0.0, 1.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 0.0, 1.0);
} else { // planar = p.xy
t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 1.0, 0.0);
}
}
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
vec2 r = vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs);
// After the rotation, so the rotation stays a rotation rather than becoming a shear.
r.y *= tex_aspect;
return r + uv_offset;
}
// sRGB -> CIELAB, matching slic3r/Utils/ColorSpaceConvert's RGB2Lab so this picks the same entry the
// bake does.
vec3 srgb_to_lab(vec3 c)
{
vec3 v = vec3(c.r > 0.04045 ? pow((c.r + 0.055) / 1.055, 2.4) : c.r / 12.92,
c.g > 0.04045 ? pow((c.g + 0.055) / 1.055, 2.4) : c.g / 12.92,
c.b > 0.04045 ? pow((c.b + 0.055) / 1.055, 2.4) : c.b / 12.92);
vec3 xyz = vec3(dot(v, vec3(0.4124, 0.3576, 0.1805)) / 0.95047,
dot(v, vec3(0.2126, 0.7152, 0.0722)),
dot(v, vec3(0.0193, 0.1192, 0.9505)) / 1.08883);
vec3 f = vec3(xyz.x > 0.008856 ? pow(xyz.x, 1.0 / 3.0) : (7.787 * xyz.x) + 16.0 / 116.0,
xyz.y > 0.008856 ? pow(xyz.y, 1.0 / 3.0) : (7.787 * xyz.y) + 16.0 / 116.0,
xyz.z > 0.008856 ? pow(xyz.z, 1.0 / 3.0) : (7.787 * xyz.z) + 16.0 / 116.0);
return vec3(116.0 * f.y - 16.0, 500.0 * (f.x - f.y), 200.0 * (f.y - f.z));
}
// Nearest printable colour to a sampled one. Quantizing per *fragment* rather than per facet is the
// whole point of this path: it shows the image at the texture's resolution instead of the mesh's,
// which is what you need while choosing a texture and placing it. The Normal view is where the
// facet-resolution truth - what actually bakes - is shown.
//
// Squared distance in Lab (CIE76) rather than the CPU's CIEDE2000: the two agree except on near-ties,
// and CIEDE2000 per fragment across 64 entries is not worth its cost in a preview.
vec3 quantize_to_palette(vec3 rgb)
{
vec3 lab = srgb_to_lab(rgb);
int best = 0;
float bd = 1.0e20;
for (int i = 0; i < 64; ++i) {
if (i >= palette_count)
break;
vec3 d = lab - palette_lab[i];
float d2 = dot(d, d);
if (d2 < bd) {
bd = d2;
best = i;
}
}
return palette_rgb[best];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
vec3 triangle_normal = normalize(cross(dFdx(model_pos.xyz), dFdy(model_pos.xyz)));
if (volume_mirrored)
triangle_normal = -triangle_normal;
// Where the colour is read from. Both branches below already compute the uv this fragment's
// *height* came from - including the parallax-marched one on the triplanar path - and the colour
// has to follow it exactly, or the colour would slide off the relief as the camera orbits.
vec2 color_uv = vec2(0.0);
bool have_uv = false;
if (use_vertex_uv) {
// Precomputed-uv (LSCM) path - Mikkelsen's surface-gradient bump ("Bump Mapping
// Unparametrized Surfaces on the GPU"). The perturbed normal is derived straight from the
// screen-space derivatives of the *sampled height* and the position, so it is scale-exact
// with no uv->mm assumption at all - which is the whole point here: an LSCM map is conformal,
// not isometric, so the local mm-per-uv varies across the chart and the earlier "one global
// 1/tiling factor" got the depth visibly wrong. dFdx(h) captures the true on-screen rate of
// change however the chart is stretched or however fine the tiling is.
//
// use_vertex_uv is a uniform, so this whole branch is uniform control flow and the texture
// derivatives are well defined; the paint weight gates the result by a plain multiply (k)
// rather than a per-fragment branch, keeping it that way.
// The dragged island's uv rides a uniform affine so its bump moves without a rebuild; every
// other vertex (island_active == 0) samples its baked uv unchanged.
vec2 uv = (island_active > 0.5)
? vec2(dot(island_delta_lin.xy, vertex_uv), dot(island_delta_lin.zw, vertex_uv)) + island_delta_tr
: vertex_uv;
color_uv = uv;
have_uv = true;
float h = texture(height_tex, uv).r;
float k = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 sigmaS = dFdx(model_pos.xyz);
vec3 sigmaT = dFdy(model_pos.xyz);
vec3 R1 = cross(sigmaT, triangle_normal);
vec3 R2 = cross(triangle_normal, sigmaS);
float det = dot(sigmaS, R1);
float dHdx = k * dFdx(h);
float dHdy = k * dFdy(h);
if (abs(det) > 1e-12)
triangle_normal = normalize(triangle_normal - (dHdx * R1 + dHdy * R2) / det);
} else if (weight > 0.0) {
// Triplanar path: uv and the tangent axes are reconstructed in-shader from the dominant
// normal component (see header). The gradient is expressed analytically because there is a
// closed-form uv here, unlike the LSCM case.
vec3 t, b;
projection_axes(triangle_normal, t, b);
// Parallax occlusion mapping: march the view ray through the height shell and shade at the
// first point where it drops below the displaced surface (see header).
float amp = (invert ? -1.0 : 1.0) * depth_mm * clamp(weight, 0.0, 1.0);
vec3 view_dir = normalize(eye_model_pos - model_pos.xyz);
float v_dot_n = dot(view_dir, triangle_normal);
vec2 uv = project_uv(model_pos.xyz, triangle_normal);
// The shell the displaced surface lives inside, as signed heights along the normal. Taken from
// both ends of h in [0, 1] so it stays correct for an inverted layer or a raised midlevel,
// where the surface sits *below* the undisplaced one.
float h_end_a = amp * (0.0 - midlevel);
float h_end_b = amp * (1.0 - midlevel);
float h_hi = max(h_end_a, h_end_b);
float h_lo = min(h_end_a, h_end_b);
// How far, in mm, sweeping the ray across the shell slides the sample point sideways. Below half
// a texel there is no parallax to find and the march would be pure cost - which is the common
// case of looking straight down at a surface.
float sweep = length(view_dir - triangle_normal * v_dot_n) * (h_hi - h_lo) / max(v_dot_n, 1e-4);
if (v_dot_n > 0.05 && sweep > 0.5 * tiling_scale * height_tex_texel.x) {
// A point at ray parameter s (model_pos + view_dir * s) sits at height s * v_dot_n above the
// undisplaced surface. Start at the top of the shell, where the ray is outside the surface
// by construction, and step inward; the crossing is what this pixel actually sees.
float s = h_hi / v_dot_n;
float ds = (h_hi - h_lo) / (v_dot_n * float(PARALLAX_STEPS));
vec2 prev_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
float prev_gap = h_hi - amp * (H_AT(prev_uv) - midlevel); // >= 0 by construction
for (int i = 0; i < PARALLAX_STEPS; ++i) {
s -= ds;
vec2 cur_uv = project_uv(model_pos.xyz + view_dir * s, triangle_normal);
float gap = s * v_dot_n - amp * (H_AT(cur_uv) - midlevel);
if (gap <= 0.0) {
// Crossed between the last two samples - interpolating the hit is what stops it
// quantising to the step size, and so what keeps the step count affordable.
uv = mix(prev_uv, cur_uv, clamp(prev_gap / max(prev_gap - gap, 1e-6), 0.0, 1.0));
break;
}
prev_uv = cur_uv;
prev_gap = gap;
}
}
color_uv = uv; // after the parallax march, so colour and relief stay registered
have_uv = true;
float hL = texture(height_tex, uv - vec2(height_tex_texel.x, 0.0)).r;
float hR = texture(height_tex, uv + vec2(height_tex_texel.x, 0.0)).r;
float hD = texture(height_tex, uv - vec2(0.0, height_tex_texel.y)).r;
float hU = texture(height_tex, uv + vec2(0.0, height_tex_texel.y)).r;
// Central difference, per uv unit (not per texel).
vec2 dh_duv = vec2((hR - hL) / (2.0 * height_tex_texel.x), (hU - hD) / (2.0 * height_tex_texel.y));
// uv -> mm is 1/tiling_scale for the triplanar projection, so this turns the uv-space
// gradient into a real surface slope.
float inv_tiling = (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float amplitude = (invert ? -1.0 : 1.0) * depth_mm * inv_tiling * clamp(weight, 0.0, 1.0);
// uv was rotated by project_uv() while t/b are the unrotated model axes, so rotate the
// gradient back into the axes' frame.
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
// One uv unit is tiling_scale mm along u but tiling_scale / tex_aspect mm along v, so the v
// component of the gradient carries the extra factor before being rotated back into t/b.
vec2 g = vec2(dh_duv.x, dh_duv.y * tex_aspect);
vec2 slope = amplitude * vec2(g.x * cs + g.y * sn, -g.x * sn + g.y * cs);
vec3 gradient = slope.x * t + slope.y * b;
gradient -= triangle_normal * dot(triangle_normal, gradient);
triangle_normal = normalize(triangle_normal - gradient);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float NdotL = max(dot(eye_normal, LIGHT_TOP_DIR), 0.0);
vec2 intensity = vec2(0.0);
intensity.x = INTENSITY_AMBIENT + NdotL * LIGHT_TOP_DIFFUSE;
vec3 position = (view_model_matrix * model_pos).xyz;
intensity.y = LIGHT_TOP_SPECULAR * pow(max(dot(-normalize(position), reflect(-LIGHT_TOP_DIR, eye_normal)), 0.0), LIGHT_TOP_SHININESS);
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = quantize_to_palette(texture(color_tex, color_uv).rgb);
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}

View File

@@ -0,0 +1,44 @@
#version 140
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
// Clipping plane, x = min z, y = max z. Used by the FFF and SLA previews to clip with a top / bottom plane.
uniform vec2 z_range;
// Clipping plane - general orientation. Used by the SLA gizmo.
uniform vec4 clipping_plane;
in vec3 v_position;
// GLModel's P3N3T2 layout (position + normal + texcoord), reused so this mesh builds and renders
// like any other GLModel rather than needing a bespoke vertex buffer. The two spare channels carry
// what the bump preview actually needs per vertex:
// v_normal.x -- the active layer's paint weight, 0 (untouched) or 1 (painted).
// v_normal.y -- 1 for a vertex of the island currently being dragged in the UV editor, else 0.
// The fragment shader applies island_delta to those vertices' uv, so a UV drag is a
// single uniform update rather than a whole-mesh rebuild (like Adjust placement).
// v_tex_coord -- the precomputed texture uv for this vertex, valid only when use_vertex_uv is set
// (i.e. the LSCM projection, where uv can't be reconstructed in the shader). The
// triplanar path ignores it and projects in the fragment shader instead.
in vec3 v_normal;
in vec2 v_tex_coord;
out vec3 clipping_planes_dots;
out vec4 model_pos;
out vec4 world_pos;
out float weight;
out float island_active;
out vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
weight = v_normal.x;
island_active = v_normal.y;
vertex_uv = v_tex_coord;
}

View File

@@ -0,0 +1,83 @@
#version 140
// UV-check overlay for the texture-displacement gizmo, drawn over the painted patch so the LSCM
// unwrap can be sanity-checked on the real 3D surface (mode set by the `mode` uniform):
// mode 0 - Checker: a procedural checkerboard sampled at the layer's uv. Even squares that stay
// square everywhere on the model mean the unwrap is low-distortion; squares that smear or
// shear reveal exactly where it stretches. Same uv the bake samples, so what you see is
// where the texture actually lands.
// mode 1 - Distortion heatmap: the per-vertex area-distortion carried in `distortion`, blue
// (compressed) -> green (ideal) -> red (stretched).
// Both are lit with the same cheap two-light diffuse the bump preview uses, so the surface still
// reads as 3D.
#define INTENSITY_CORRECTION 0.6
const vec3 LIGHT_TOP_DIR = vec3(-0.4574957, 0.4574957, 0.7624929);
#define LIGHT_TOP_DIFFUSE (0.8 * INTENSITY_CORRECTION)
const vec3 LIGHT_FRONT_DIR = vec3(0.6985074, 0.1397015, 0.6985074);
#define LIGHT_FRONT_DIFFUSE (0.3 * INTENSITY_CORRECTION)
#define INTENSITY_AMBIENT 0.3
const vec3 ZERO = vec3(0.0, 0.0, 0.0);
uniform mat3 view_normal_matrix;
uniform bool volume_mirrored;
uniform int mode; // 0 checker, 1 distortion
uniform float checker_freq; // checker squares per uv unit (one uv unit == one texture tile)
uniform float tiling_scale;
uniform float rotation_rad;
uniform vec2 uv_offset;
uniform bool use_vertex_uv;
in vec3 clipping_planes_dots;
in vec4 model_pos;
in vec4 world_pos;
in float distortion;
in vec2 vertex_uv;
out vec4 out_color;
vec2 project_uv(vec3 p, vec3 n)
{
vec3 an = abs(n);
vec2 planar = (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
planar *= (tiling_scale > 1e-6) ? (1.0 / tiling_scale) : 1.0;
float cs = cos(rotation_rad);
float sn = sin(rotation_rad);
return vec2(planar.x * cs - planar.y * sn, planar.x * sn + planar.y * cs) + uv_offset;
}
// Blue -> cyan -> green -> yellow -> red over t in [0,1].
vec3 heatmap(float t)
{
t = clamp(t, 0.0, 1.0);
return clamp(vec3(1.5 - abs(4.0 * t - 3.0),
1.5 - abs(4.0 * t - 2.0),
1.5 - abs(4.0 * t - 1.0)), 0.0, 1.0);
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
discard;
vec3 triangle_normal = normalize(cross(dFdx(model_pos.xyz), dFdy(model_pos.xyz)));
if (volume_mirrored)
triangle_normal = -triangle_normal;
vec3 base;
if (mode == 1) {
base = heatmap(distortion);
} else {
vec2 uv = use_vertex_uv ? vertex_uv : project_uv(model_pos.xyz, triangle_normal);
vec2 c = floor(uv * checker_freq);
float check = mod(c.x + c.y, 2.0);
// Two distinct greys, plus a faint tint on one set so orientation is readable at a glance.
base = (check < 0.5) ? vec3(0.22, 0.23, 0.26) : vec3(0.82, 0.83, 0.86);
}
vec3 eye_normal = normalize(view_normal_matrix * triangle_normal);
float intensity = INTENSITY_AMBIENT + max(dot(eye_normal, LIGHT_TOP_DIR), 0.0) * LIGHT_TOP_DIFFUSE
+ max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0) * LIGHT_FRONT_DIFFUSE;
out_color = vec4(base * intensity, 1.0);
}

View File

@@ -0,0 +1,36 @@
#version 140
// Vertex stage for the UV-check overlay (checker / distortion heatmap) drawn over the painted patch
// by GLGizmoTextureDisplacement. Reuses GLModel's P3N3T2 layout so it needs no bespoke buffer:
// v_normal.x - per-vertex UV distortion (uv-area / surface-area ratio, remapped so 0.5 = ideal);
// only the distortion mode reads it.
// v_tex_coord - precomputed texture uv, valid only when use_vertex_uv is set (LSCM); the checker
// mode reconstructs uv in the fragment shader otherwise.
uniform mat4 view_model_matrix;
uniform mat4 projection_matrix;
uniform mat4 volume_world_matrix;
uniform vec2 z_range;
uniform vec4 clipping_plane;
in vec3 v_position;
in vec3 v_normal;
in vec2 v_tex_coord;
out vec3 clipping_planes_dots;
out vec4 model_pos;
out vec4 world_pos;
out float distortion;
out vec2 vertex_uv;
void main()
{
model_pos = vec4(v_position, 1.0);
world_pos = volume_world_matrix * model_pos;
gl_Position = projection_matrix * view_model_matrix * model_pos;
clipping_planes_dots = vec3(dot(world_pos, clipping_plane), world_pos.z - z_range.x, z_range.y - world_pos.z);
distortion = v_normal.x;
vertex_uv = v_tex_coord;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

View File

@@ -462,6 +462,24 @@ set(lisbslic3r_sources
Tesselate.cpp
Tesselate.hpp
TextConfiguration.hpp
TextureDisplacement.cpp
TextureDisplacement.hpp
TextureBake/TextureBakeIndex.cpp
TextureBake/TextureBakeIndex.hpp
TextureBake/TextureBakeSubdivide.cpp
TextureBake/TextureBakeSubdivide.hpp
TextureBake/TextureBakeRegularize.cpp
TextureBake/TextureBakeRegularize.hpp
TextureBake/TextureBakeDisplace.cpp
TextureBake/TextureBakeDisplace.hpp
TextureBake/TextureBakeDecimate.cpp
TextureBake/TextureBakeDecimate.hpp
TextureBake/TextureBakeRepair.cpp
TextureBake/TextureBakeRepair.hpp
TextureBake/TextureBakePipeline.cpp
TextureBake/TextureBakePipeline.hpp
TextureBake/TextureBakeMesh.cpp
TextureBake/TextureBakeMesh.hpp
Thread.cpp
Thread.hpp
Time.cpp

View File

@@ -78,7 +78,7 @@ struct PlateBBoxData
int first_extruder = 0;
float nozzle_diameter = 0.4;
std::string bed_type;
float first_layer_time = 0.0f;
float first_layer_time;
// version 1: use view type ColorPrint (filament color)
// version 2: use view type FilamentId (filament id)
int version = 2;

View File

@@ -165,6 +165,7 @@ public:
//BBS
unsigned int m_last_additional_fan_speed;
int m_last_bed_temperature;
bool m_last_bed_temperature_reached;
double m_lifted;

View File

@@ -26,6 +26,13 @@
#include <CGAL/property_map.h>
#include <CGAL/boost/graph/copy_face_graph.h>
#include <CGAL/boost/graph/Face_filtered_graph.h>
// For parameterize_lscm()
#include <CGAL/Polygon_mesh_processing/border.h>
#include <CGAL/Polygon_mesh_processing/connected_components.h>
#include <CGAL/Polygon_mesh_processing/detect_features.h>
#include <CGAL/Surface_mesh_parameterization/Error_code.h>
#include <CGAL/Surface_mesh_parameterization/LSCM_parameterizer_3.h>
#include <CGAL/Surface_mesh_parameterization/parameterize.h>
// BBS: for boolean using mcut
#include "mcut/include/mcut/mcut.h"
@@ -249,6 +256,149 @@ indexed_triangle_set cgal_to_indexed_triangle_set(const CGALMesh &cgalmesh)
return cgal_to_indexed_triangle_set(cgalmesh.m);
}
// /////////////////////////////////////////////////////////////////////////////
// Isotropic remeshing
// /////////////////////////////////////////////////////////////////////////////
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
unsigned n_iterations, double sharp_angle_deg)
{
if (mesh.indices.empty() || target_edge_length <= 0.0)
return mesh;
_EpicMesh cgal_mesh;
triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh);
if (cgal_mesh.is_empty() || cgal_mesh.number_of_faces() == 0)
return mesh;
// Surface_mesh::add_face() refuses any face that would make the mesh non-manifold and returns a
// null descriptor instead. Remeshing a mesh that silently lost faces that way produces holes in
// the output, so bail out and let the caller report it rather than hand back a punctured model.
if (cgal_mesh.number_of_faces() != mesh.indices.size())
return mesh;
using edge_descriptor = boost::graph_traits<_EpicMesh>::edge_descriptor;
try {
// Sharp edges and open borders are pinned before remeshing. Without that, the tangential
// relaxation pass slides vertices along the surface and rounds every hard feature off - a
// cube comes back with wobbly, eroded edges, which is the most visible way "remeshing does
// not work properly". protect_constraints() forbids splitting or collapsing them, but it
// requires each constrained edge to already be shorter than 4/3 * target, hence the split
// first (passing the map so the halves inherit the constraint). This mirrors CGAL's own
// isotropic_remeshing example.
auto ecm = cgal_mesh.add_property_map<edge_descriptor, bool>("e:is_constrained", false).first;
if (sharp_angle_deg > 0.0)
CGALProc::detect_sharp_edges(cgal_mesh, sharp_angle_deg, ecm);
for (edge_descriptor e : edges(cgal_mesh)) {
const auto h = halfedge(e, cgal_mesh);
if (is_border(h, cgal_mesh) || is_border(opposite(h, cgal_mesh), cgal_mesh))
put(ecm, e, true);
}
std::vector<edge_descriptor> constrained;
for (edge_descriptor e : edges(cgal_mesh))
if (get(ecm, e))
constrained.push_back(e);
if (!constrained.empty())
CGALProc::split_long_edges(constrained, target_edge_length, cgal_mesh,
CGALParams::edge_is_constrained_map(ecm));
CGALProc::isotropic_remeshing(faces(cgal_mesh), target_edge_length, cgal_mesh,
CGALParams::number_of_iterations(n_iterations)
.edge_is_constrained_map(ecm)
.protect_constraints(true));
} catch (const std::exception &) {
return mesh; // CGAL throws on some non-manifold / degenerate inputs; leave the mesh untouched
}
if (cgal_mesh.number_of_faces() == 0)
return mesh;
// isotropic_remeshing edits in place, and its edge collapses only *mark* vertices and faces as
// removed - the underlying arrays keep the holes until the garbage is collected. That matters
// because cgal_to_indexed_triangle_set() numbers its output vertices by iteration order (which
// skips removed slots) while reading each face's corner as the raw integer value of the vertex
// descriptor (which does not). Past the first collapse the two disagree, so every triangle
// points at the wrong vertices, and any descriptor beyond the live vertex count is dropped
// together with its triangle. Compacting first makes descriptor == iteration order again.
cgal_mesh.collect_garbage();
return cgal_to_indexed_triangle_set(cgal_mesh);
}
// /////////////////////////////////////////////////////////////////////////////
// UV parameterization
// /////////////////////////////////////////////////////////////////////////////
std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &mesh)
{
namespace SMP = CGAL::Surface_mesh_parameterization;
if (mesh.indices.empty())
return std::nullopt;
_EpicMesh cgal_mesh;
triangle_mesh_to_cgal(mesh.vertices, mesh.indices, cgal_mesh);
using vertex_descriptor = boost::graph_traits<_EpicMesh>::vertex_descriptor;
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
// LSCM assumes a single topological disk: one connected component, one boundary loop. A patch
// with several disconnected painted islands, or with a hole in it, doesn't qualify -- bail out
// rather than silently parameterizing just one arbitrary piece of it.
{
std::vector<std::size_t> component_id(num_faces(cgal_mesh));
const std::size_t num_components = CGAL::Polygon_mesh_processing::connected_components(
cgal_mesh, CGAL::make_property_map(component_id));
if (num_components != 1)
return std::nullopt;
}
const halfedge_descriptor border = CGAL::Polygon_mesh_processing::longest_border(cgal_mesh).first;
if (border == halfedge_descriptor())
return std::nullopt; // no boundary at all -- a closed patch, which isn't a disk either
// ...and exactly one boundary loop. One connected component is not enough on its own: a patch with
// a hole in it (paint a ring, or erase the middle of a stroke) is a single component with two
// loops, and LSCM will happily "parameterize" it into an overlapping, folded-over chart rather
// than fail. Walk the border halfedges and check every one of them belongs to the longest loop.
{
std::size_t border_halfedges = 0;
for (halfedge_descriptor h : halfedges(cgal_mesh))
if (is_border(h, cgal_mesh))
++border_halfedges;
std::size_t loop_length = 0;
halfedge_descriptor h = border;
do {
++loop_length;
h = next(h, cgal_mesh);
} while (h != border && loop_length <= border_halfedges);
if (loop_length != border_halfedges)
return std::nullopt; // more than one boundary loop -- not a topological disk
}
using Point_2 = EpicKernel::Point_2;
using UV_pmap = _EpicMesh::Property_map<vertex_descriptor, Point_2>;
UV_pmap uv_map = cgal_mesh.add_property_map<vertex_descriptor, Point_2>("h:uv", Point_2(0, 0)).first;
using Parameterizer = SMP::LSCM_parameterizer_3<_EpicMesh>;
const SMP::Error_code err = SMP::parameterize(cgal_mesh, Parameterizer(), border, uv_map);
if (err != SMP::OK)
return std::nullopt;
// triangle_mesh_to_cgal() adds vertices in the exact same order as mesh.vertices (see above),
// and Surface_mesh assigns indices sequentially on insertion into a fresh mesh, so a
// vertex_descriptor's index here is guaranteed to match the original input vertex index --
// the same assumption cgal_to_indexed_triangle_set() above already relies on.
std::vector<Vec2f> result(mesh.vertices.size(), Vec2f::Zero());
for (vertex_descriptor vd : vertices(cgal_mesh)) {
const std::size_t idx = std::size_t(vd);
if (idx < result.size()) {
const Point_2 &uv = uv_map[vd];
result[idx] = Vec2f(float(uv.x()), float(uv.y()));
}
}
return result;
}
// /////////////////////////////////////////////////////////////////////////////
// Boolean operations for CGAL meshes
// /////////////////////////////////////////////////////////////////////////////

View File

@@ -3,6 +3,8 @@
#include <memory>
#include <exception>
#include <optional>
#include <vector>
#include <libslic3r/TriangleMesh.hpp>
#include <Eigen/Geometry>
@@ -73,6 +75,22 @@ bool empty(const CGALMesh &mesh);
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
// component with exactly one boundary loop, true for a typical single brush stroke/patch but not
// guaranteed for multiple disconnected painted islands merged into one mesh.
std::optional<std::vector<Vec2f>> parameterize_lscm(const indexed_triangle_set &mesh);
// Isotropic remeshing (CGAL): rebuilds the mesh so its triangles are close to a uniform target edge
// length, splitting oversized triangles and collapsing undersized ones. Used to even out a model with
// wildly varying triangle sizes so texture displacement has a consistent vertex density to work with.
// Edges whose dihedral angle exceeds `sharp_angle_deg`, and any open border, are held fixed so hard
// features survive instead of being eroded by the relaxation pass; pass 0 to remesh everything.
// Returns the input unchanged if remeshing fails (e.g. a non-manifold or self-intersecting input).
indexed_triangle_set remesh_isotropic(const indexed_triangle_set &mesh, double target_edge_length,
unsigned n_iterations = 3, double sharp_angle_deg = 40.0);
}
namespace mcut {

View File

@@ -2056,6 +2056,11 @@ void ModelVolume::reset_extra_facets()
this->seam_facets.reset();
this->mmu_segmentation_facets.reset();
this->fuzzy_skin_facets.reset();
// Texture-displacement paint data has no remap-across-topology-change support yet (see
// build_texture_displacement()'s documented limitation), so it must be dropped here rather
// than left referring to a mesh that no longer matches it.
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).reset();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const

View File

@@ -19,6 +19,7 @@
#include "TextConfiguration.hpp"
#include "EmbossShape.hpp"
#include "TriangleSelector.hpp"
#include "TextureDisplacement.hpp"
//BBS: add bbs 3mf
#include "Format/bbs_3mf.hpp"
@@ -28,6 +29,7 @@
#include "Format/STL.hpp"
#include "Format/OBJ.hpp"
#include <array>
#include <map>
#include <memory>
#include <string>
@@ -883,6 +885,69 @@ public:
// List of mesh facets painted for fuzzy skin.
FacetsAnnotation fuzzy_skin_facets;
// One independent paint mask per texture-displacement layer slot (see texture_displacement_layers
// below). Unlike the other facets fields above, a triangle may be painted (ENFORCER) in more
// than one of these simultaneously -- that overlap is what makes the layers "blend".
//
// These are 8 plain named fields rather than a std::array<FacetsAnnotation, N>: FacetsAnnotation's
// default/copy constructors are private and friended only to ModelVolume, but std::array's own
// implicitly-defined default/copy constructors are generated with std::array's access rights,
// not ModelVolume's -- so an array of FacetsAnnotation ends up with its default/copy
// constructors implicitly deleted regardless of the friend declaration. Use
// texture_displacement_facet(slot) below for array-like indexed access.
FacetsAnnotation texture_displacement_facets_0;
FacetsAnnotation texture_displacement_facets_1;
FacetsAnnotation texture_displacement_facets_2;
FacetsAnnotation texture_displacement_facets_3;
FacetsAnnotation texture_displacement_facets_4;
FacetsAnnotation texture_displacement_facets_5;
FacetsAnnotation texture_displacement_facets_6;
FacetsAnnotation texture_displacement_facets_7;
FacetsAnnotation& texture_displacement_facet(int slot) {
switch (slot) {
case 0: return texture_displacement_facets_0;
case 1: return texture_displacement_facets_1;
case 2: return texture_displacement_facets_2;
case 3: return texture_displacement_facets_3;
case 4: return texture_displacement_facets_4;
case 5: return texture_displacement_facets_5;
case 6: return texture_displacement_facets_6;
default: assert(slot == 7); return texture_displacement_facets_7;
}
}
const FacetsAnnotation& texture_displacement_facet(int slot) const { return const_cast<ModelVolume*>(this)->texture_displacement_facet(slot); }
// Small helpers for the constructor asserts below (kept out of line-noise at each call site).
bool texture_displacement_facets_ids_valid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).id().valid() || texture_displacement_facet(i).id() == this->id())
return false;
return true;
}
bool texture_displacement_facets_ids_invalid() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (texture_displacement_facet(i).id().valid())
return false;
return true;
}
bool texture_displacement_facets_all_empty() const {
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
if (!texture_displacement_facet(i).empty())
return false;
return true;
}
// Texture assets (height maps) and their projection/displacement parameters. Element order
// is not meaningful for baking (layers are applied in TextureDisplacementLayer::slot order,
// see build_texture_displacement()); it only reflects UI insertion order.
std::vector<TextureDisplacementLayer> texture_displacement_layers;
// Whole-stack displacement settings (border handling, post-process smoothing) - see
// TextureDisplacementOptions. They live beside the layers rather than on one of them because
// they are not a property of any single layer.
TextureDisplacementOptions texture_displacement_options;
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1020,13 +1085,18 @@ public:
this->seam_facets.set_new_unique_id();
this->mmu_segmentation_facets.set_new_unique_id();
this->fuzzy_skin_facets.set_new_unique_id();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
this->texture_displacement_facet(i).set_new_unique_id();
}
bool is_fdm_support_painted() const { return !this->supported_facets.empty(); }
bool is_seam_painted() const { return !this->seam_facets.empty(); }
bool is_mm_painted() const { return !this->mmu_segmentation_facets.empty(); }
bool is_fuzzy_skin_painted() const { return !this->fuzzy_skin_facets.empty(); }
bool is_any_painted() const { return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted(); }
bool is_texture_displacement_painted() const { return !this->texture_displacement_facets_all_empty(); }
bool is_any_painted() const {
return is_fdm_support_painted() || is_seam_painted() || is_mm_painted() || is_fuzzy_skin_painted() || is_texture_displacement_painted();
}
// Orca: Implement prusa's filament shrink compensation approach
// Returns 0-based indices of extruders painted by multi-material painting gizmo.
@@ -1079,6 +1149,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1095,6 +1166,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1109,6 +1181,7 @@ private:
assert(this->seam_facets.id().valid());
assert(this->mmu_segmentation_facets.id().valid());
assert(this->fuzzy_skin_facets.id().valid());
assert(this->texture_displacement_facets_ids_valid());
assert(this->id() != this->config.id());
assert(this->id() != this->supported_facets.id());
assert(this->id() != this->seam_facets.id());
@@ -1122,7 +1195,14 @@ private:
name(other.name), source(other.source), m_mesh(other.m_mesh), m_convex_hull(other.m_convex_hull),
config(other.config), m_type(other.m_type), object(object), m_transformation(other.m_transformation),
supported_facets(other.supported_facets), seam_facets(other.seam_facets), mmu_segmentation_facets(other.mmu_segmentation_facets),
fuzzy_skin_facets(other.fuzzy_skin_facets), cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
fuzzy_skin_facets(other.fuzzy_skin_facets),
texture_displacement_facets_0(other.texture_displacement_facets_0), texture_displacement_facets_1(other.texture_displacement_facets_1),
texture_displacement_facets_2(other.texture_displacement_facets_2), texture_displacement_facets_3(other.texture_displacement_facets_3),
texture_displacement_facets_4(other.texture_displacement_facets_4), texture_displacement_facets_5(other.texture_displacement_facets_5),
texture_displacement_facets_6(other.texture_displacement_facets_6), texture_displacement_facets_7(other.texture_displacement_facets_7),
texture_displacement_layers(other.texture_displacement_layers),
texture_displacement_options(other.texture_displacement_options),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{
assert(this->id().valid());
assert(this->config.id().valid());
@@ -1175,6 +1255,8 @@ private:
assert(this->seam_facets.empty());
assert(this->mmu_segmentation_facets.empty());
assert(this->fuzzy_skin_facets.empty());
assert(this->texture_displacement_facets_all_empty());
assert(this->texture_displacement_layers.empty());
}
ModelVolume& operator=(ModelVolume &rhs) = delete;
@@ -1182,13 +1264,17 @@ private:
friend class cereal::access;
friend class UndoRedo::StackImpl;
// Used for deserialization, therefore no IDs are allocated.
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1), object(nullptr) {
ModelVolume() : ObjectBase(-1), config(-1), supported_facets(-1), seam_facets(-1), mmu_segmentation_facets(-1), fuzzy_skin_facets(-1),
texture_displacement_facets_0(-1), texture_displacement_facets_1(-1), texture_displacement_facets_2(-1), texture_displacement_facets_3(-1),
texture_displacement_facets_4(-1), texture_displacement_facets_5(-1), texture_displacement_facets_6(-1), texture_displacement_facets_7(-1),
object(nullptr) {
assert(this->id().invalid());
assert(this->config.id().invalid());
assert(this->supported_facets.id().invalid());
assert(this->seam_facets.id().invalid());
assert(this->mmu_segmentation_facets.id().invalid());
assert(this->fuzzy_skin_facets.id().invalid());
assert(this->texture_displacement_facets_ids_invalid());
}
template<class Archive> void load(Archive &ar) {
bool has_convex_hull;
@@ -1208,6 +1294,13 @@ private:
mesh_changed |= t != mmu_segmentation_facets.timestamp();
cereal::load_by_value(ar, fuzzy_skin_facets);
mesh_changed |= t != fuzzy_skin_facets.timestamp();
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i) {
FacetsAnnotation &f = texture_displacement_facet(i);
Timestamp tf = f.timestamp();
cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp();
}
ar(texture_displacement_layers, texture_displacement_options);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1229,6 +1322,9 @@ private:
cereal::save_by_value(ar, seam_facets);
cereal::save_by_value(ar, mmu_segmentation_facets);
cereal::save_by_value(ar, fuzzy_skin_facets);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
cereal::save_by_value(ar, texture_displacement_facet(i));
ar(texture_displacement_layers, texture_displacement_options);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);

View File

@@ -0,0 +1,478 @@
#include "TextureBakeDecimate.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <queue>
namespace Slic3r {
namespace TextureBake {
namespace {
// Symmetric 4x4 quadric, as its 10 upper-triangle values.
struct Quadric
{
std::array<double, 10> q{};
void add_plane(double a, double b, double c, double d)
{
q[0] += a * a; q[1] += a * b; q[2] += a * c; q[3] += a * d;
q[4] += b * b; q[5] += b * c; q[6] += b * d;
q[7] += c * c; q[8] += c * d;
q[9] += d * d;
}
void operator+=(const Quadric &o)
{
for (int i = 0; i < 10; ++i)
q[size_t(i)] += o.q[size_t(i)];
}
double eval(double x, double y, double z) const
{
return q[0] * x * x + 2 * q[1] * x * y + 2 * q[2] * x * z + 2 * q[3] * x +
q[4] * y * y + 2 * q[5] * y * z + 2 * q[6] * y +
q[7] * z * z + 2 * q[8] * z + q[9];
}
};
double eval_sum(const std::vector<Quadric> &qs, int v1, int v2, const Vec3d &p)
{
return qs[size_t(v1)].eval(p.x(), p.y(), p.z()) + qs[size_t(v2)].eval(p.x(), p.y(), p.z());
}
// The position minimising the summed quadric, if the system is well conditioned enough to trust.
bool solve_q(const std::vector<Quadric> &qs, int v1, int v2, Vec3d &out)
{
const auto &A = qs[size_t(v1)].q;
const auto &B = qs[size_t(v2)].q;
const double a00 = A[0] + B[0], a01 = A[1] + B[1], a02 = A[2] + B[2];
const double a11 = A[4] + B[4], a12 = A[5] + B[5], a22 = A[7] + B[7];
const double b0 = -(A[3] + B[3]), b1 = -(A[6] + B[6]), b2 = -(A[8] + B[8]);
const double det = a00 * (a11 * a22 - a12 * a12) - a01 * (a01 * a22 - a12 * a02) +
a02 * (a01 * a12 - a11 * a02);
const double max_el = std::max({ std::abs(a00), std::abs(a01), std::abs(a02), std::abs(a11),
std::abs(a12), std::abs(a22) });
// Scaled with the matrix, so it means the same at any model scale.
const double threshold = max_el * max_el * max_el * 1e-10;
if (std::abs(det) < std::max(threshold, 1e-30))
return false;
const double inv = 1.0 / det;
out.x() = inv * (b0 * (a11 * a22 - a12 * a12) - a01 * (b1 * a22 - a12 * b2) + a02 * (b1 * a12 - a11 * b2));
out.y() = inv * (a00 * (b1 * a22 - a12 * b2) - b0 * (a01 * a22 - a12 * a02) + a02 * (a01 * b2 - b1 * a02));
out.z() = inv * (a00 * (a11 * b2 - b1 * a12) - a01 * (a01 * b2 - b1 * a02) + b0 * (a01 * a12 - a11 * a02));
return true;
}
Vec3d face_normal_unit(const std::vector<Vec3d> &pos, int a, int b, int c)
{
const Vec3d n = (pos[size_t(b)] - pos[size_t(a)]).cross(pos[size_t(c)] - pos[size_t(a)]);
const double len = n.norm();
return (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
}
// Versions are captured at push time; a mismatch on pop means a later collapse invalidated the entry.
// Lazy deletion, far cheaper than removing entries eagerly.
struct HeapEntry
{
double cost;
int v1, v2;
uint32_t ver1, ver2;
Vec3d p;
bool operator>(const HeapEntry &o) const { return cost > o.cost; }
};
} // namespace
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat,
double harvest_tol, const std::vector<uint8_t> &locked_faces,
const DecimateProgressFn &on_progress)
{
DecimateResult result;
const size_t n = geometry.pos.size();
if (n < 3) {
result.geometry = geometry;
return result;
}
// The finest grid. Anything coarser fuses distinct fine-feature vertices on a displaced mesh,
// leaving it non-manifold before decimation starts and producing open edges afterwards.
QuantizedPointMap vert_map(WELD_GRID_DECIMATION, std::min(n, size_t(1) << 22));
std::vector<Vec3d> pos;
std::vector<int> remap(n);
for (size_t i = 0; i < n; ++i) {
const int idx = vert_map.get_or_set(geometry.pos[i], int(pos.size()));
if (vert_map.inserted())
pos.push_back(geometry.pos[i].cast<double>());
remap[i] = idx;
}
const size_t vert_count = pos.size();
const size_t face_count = n / 3;
std::vector<int> faces(face_count * 3);
for (size_t i = 0; i < n; ++i)
faces[i] = remap[i];
if (face_count <= target_triangles && !harvest_flat) {
result.geometry = geometry;
return result;
}
// An edge with a locked endpoint never reaches the heap.
std::vector<uint8_t> locked_vert;
size_t locked_face_count = 0;
if (!locked_faces.empty()) {
locked_vert.assign(vert_count, 0);
for (size_t f = 0; f < face_count && f < locked_faces.size(); ++f) {
if (!locked_faces[f])
continue;
++locked_face_count;
for (int k = 0; k < 3; ++k)
locked_vert[size_t(faces[f * 3 + size_t(k)])] = 1;
}
}
// With the locked faces alone at the target, chasing it would grind the free region to its guard
// limit for nothing - harvest only, and say so.
const bool locked_over_budget =
!locked_vert.empty() && face_count > target_triangles && locked_face_count >= target_triangles;
result.locked_over_budget = locked_over_budget;
if (locked_over_budget && !harvest_flat) {
result.geometry = geometry;
return result;
}
std::vector<Quadric> quadrics(vert_count);
for (size_t f = 0; f < face_count; ++f) {
const int a = faces[f * 3], b = faces[f * 3 + 1], c = faces[f * 3 + 2];
if (a < 0)
continue;
const Vec3d nrm = face_normal_unit(pos, a, b, c);
if (nrm.isZero())
continue;
const double d = -nrm.dot(pos[size_t(a)]);
for (const int v : { a, b, c })
quadrics[size_t(v)].add_plane(nrm.x(), nrm.y(), nrm.z(), d);
}
// Two penalty planes per endpoint on a sharp interior edge, each perpendicular to one adjacent
// face and containing the edge, constraining the vertex to the crease line.
{
struct EdgeRec { int va, vb, f0, f1; uint8_t count; };
std::vector<EdgeRec> edges;
QuantizedPointMap edge_idx(1.0, std::min(face_count * 3, size_t(1) << 22));
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
for (int e = 0; e < 3; ++e) {
const int va = faces[f * 3 + size_t(e)];
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
const int lo = std::min(va, vb), hi = std::max(va, vb);
const int ei = edge_idx.get_or_set_key(lo, hi, 0, int(edges.size()));
if (edge_idx.inserted())
edges.push_back({ lo, hi, int(f), -1, 1 });
else if (edges[size_t(ei)].count == 1) {
edges[size_t(ei)].f1 = int(f);
edges[size_t(ei)].count = 2;
} else
// Non-manifold; never feeds a crease.
edges[size_t(ei)].count = 3;
}
}
const double sqrt_w = std::sqrt(DECIMATE_CREASE_WEIGHT);
for (const EdgeRec &er : edges) {
if (er.count != 2)
continue; // boundary or non-manifold
const Vec3d n0 = face_normal_unit(pos, faces[size_t(er.f0) * 3], faces[size_t(er.f0) * 3 + 1],
faces[size_t(er.f0) * 3 + 2]);
const Vec3d n1 = face_normal_unit(pos, faces[size_t(er.f1) * 3], faces[size_t(er.f1) * 3 + 1],
faces[size_t(er.f1) * 3 + 2]);
if (n0.dot(n1) >= DECIMATE_CREASE_COS)
continue; // smooth enough to be no crease
const Vec3d e = pos[size_t(er.vb)] - pos[size_t(er.va)];
const double elen = e.norm();
if (elen <= 0.0)
continue;
const Vec3d ed = e / elen;
for (const Vec3d &fn : { n0, n1 }) {
Vec3d pn = fn.cross(ed);
const double plen = pn.norm();
if (plen < 1e-10)
continue; // edge parallel to the face normal
pn /= plen;
const double d = -pn.dot(pos[size_t(er.va)]);
// sqrt(w) on the inputs gives w times the accumulated products.
for (const int v : { er.va, er.vb })
quadrics[size_t(v)].add_plane(pn.x() * sqrt_w, pn.y() * sqrt_w, pn.z() * sqrt_w,
d * sqrt_w);
}
}
}
// Vertex-face incidence as intrusive linked lists of slots over flat arrays.
const size_t S = face_count * 3;
std::vector<int> vf_head(vert_count, -1), slot_face(S), slot_vert(S), slot_next(S, -1),
slot_prev(S, -1), face_slot(S, -1);
for (size_t f = 0; f < face_count; ++f)
for (int k = 0; k < 3; ++k) {
const int s = int(f) * 3 + k;
const int v = faces[size_t(s)];
slot_face[size_t(s)] = int(f);
slot_vert[size_t(s)] = v;
slot_next[size_t(s)] = vf_head[size_t(v)];
slot_prev[size_t(s)] = -1;
if (vf_head[size_t(v)] >= 0)
slot_prev[size_t(vf_head[size_t(v)])] = s;
vf_head[size_t(v)] = s;
face_slot[size_t(s)] = s;
}
const auto unlink_slot = [&](int s) {
const int p = slot_prev[size_t(s)], nx = slot_next[size_t(s)];
if (p >= 0) slot_next[size_t(p)] = nx;
else vf_head[size_t(slot_vert[size_t(s)])] = nx;
if (nx >= 0) slot_prev[size_t(nx)] = p;
};
const auto move_slot = [&](int s, int nv) {
unlink_slot(s);
slot_next[size_t(s)] = vf_head[size_t(nv)];
slot_prev[size_t(s)] = -1;
if (vf_head[size_t(nv)] >= 0)
slot_prev[size_t(vf_head[size_t(nv)])] = s;
vf_head[size_t(nv)] = s;
slot_vert[size_t(s)] = nv;
};
std::vector<uint8_t> active(vert_count, 1);
std::vector<uint32_t> version(vert_count, 0);
std::vector<uint32_t> nb_stamp(vert_count, 0), lk_stamp(vert_count, 0);
uint32_t epoch = 1, lk_epoch = 1;
size_t active_faces = face_count;
std::priority_queue<HeapEntry, std::vector<HeapEntry>, std::greater<HeapEntry>> heap;
const auto push_edge = [&](int v1, int v2) {
Vec3d p;
if (!solve_q(quadrics, v1, v2, p)) {
const Vec3d mid = (pos[size_t(v1)] + pos[size_t(v2)]) * 0.5;
const double e1 = eval_sum(quadrics, v1, v2, pos[size_t(v1)]);
const double e2 = eval_sum(quadrics, v1, v2, pos[size_t(v2)]);
const double em = eval_sum(quadrics, v1, v2, mid);
const double emin = std::min({ e1, e2, em });
const double etol = emin * 1e-2 + 1e-12;
// The midpoint when the three are near-equal, i.e. flat: it moves adjacent triangles
// least, so fewer normal flips and no stalling on coplanar geometry.
if (em <= emin + etol) p = mid;
else if (e1 <= e2) p = pos[size_t(v1)];
else p = pos[size_t(v2)];
}
// Where quadric costs are all near zero, shorter edges first keeps triangle quality up.
const double len2 = (pos[size_t(v2)] - pos[size_t(v1)]).squaredNorm();
heap.push({ eval_sum(quadrics, v1, v2, p) + len2 * 1e-8, v1, v2, version[size_t(v1)],
version[size_t(v2)], p });
};
{
QuantizedPointMap seed_seen(1.0, std::min(face_count * 3, size_t(1) << 22));
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
for (int e = 0; e < 3; ++e) {
const int va = faces[f * 3 + size_t(e)];
const int vb = faces[f * 3 + size_t((e + 1) % 3)];
if (!locked_vert.empty() && (locked_vert[size_t(va)] || locked_vert[size_t(vb)]))
continue;
seed_seen.get_or_set_key(std::min(va, vb), std::max(va, vb), 0, 1);
if (seed_seen.inserted())
push_edge(va, vb);
}
}
}
// 0 means a stale entry, 1 a boundary edge, 2 or more safe.
const auto shared_face_count = [&](int v1, int v2) {
int count = 0;
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k)
if (faces[size_t(f) * 3 + size_t(k)] == v2) {
if (++count >= 2)
return 2;
break;
}
}
return count;
};
// Safe only when the sole common neighbours of the endpoints are the apexes of the faces the edge
// already shares; any other would pile a third triangle onto an edge after the collapse.
const auto has_link_violation = [&](int v1, int v2, uint32_t ep) {
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k)
if (const int x = faces[size_t(f) * 3 + size_t(k)]; x != v1)
lk_stamp[size_t(x)] = ep;
}
int shared = 0;
for (int s = vf_head[size_t(v1)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
const int a = faces[size_t(f) * 3], b = faces[size_t(f) * 3 + 1], c = faces[size_t(f) * 3 + 2];
if (a == v2 || b == v2 || c == v2) {
++shared;
const int apex = (a != v1 && a != v2) ? a : (b != v1 && b != v2) ? b : c;
lk_stamp[size_t(apex)] = ep + 1; // a legal shared-face apex
}
}
if (shared > 2)
return true; // already non-manifold
for (int s = vf_head[size_t(v2)]; s >= 0; s = slot_next[size_t(s)]) {
const int f = slot_face[size_t(s)];
if (faces[size_t(f) * 3] < 0)
continue;
for (int k = 0; k < 3; ++k) {
const int x = faces[size_t(f) * 3 + size_t(k)];
if (x != v2 && x != v1 && lk_stamp[size_t(x)] == ep)
return true;
}
}
return false;
};
// Squared-dot, so no square root or division. Faces containing the other endpoint are the ones
// being removed, so they are skipped.
const auto check_flipped = [&](int vc, int vo, const Vec3d &np) {
for (int s = vf_head[size_t(vc)]; s >= 0; s = slot_next[size_t(s)]) {
const size_t f = size_t(slot_face[size_t(s)]);
if (faces[f * 3] < 0)
continue;
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
if (fa == vo || fb == vo || fc == vo)
continue;
const Vec3d oa = pos[size_t(fa)], ob = pos[size_t(fb)], oc = pos[size_t(fc)];
const Vec3d on = (ob - oa).cross(oc - oa);
const Vec3d na = (fa == vc) ? np : oa;
const Vec3d nb = (fb == vc) ? np : ob;
const Vec3d nc = (fc == vc) ? np : oc;
const Vec3d nn = (nb - na).cross(nc - na);
const double raw = on.dot(nn);
if (raw < 0.0)
return true;
if (raw * raw < DECIMATE_FLIP_DOT * DECIMATE_FLIP_DOT * on.squaredNorm() * nn.squaredNorm())
return true;
}
return false;
};
const size_t init_faces = active_faces;
const size_t to_remove = std::max<size_t>(1, init_faces > target_triangles
? init_faces - target_triangles : init_faces);
const double harvest_ceil = harvest_tol * harvest_tol;
bool reached_target = locked_over_budget;
double last_progress = 0.0;
while (!heap.empty()) {
if (active_faces <= target_triangles) {
if (!harvest_flat)
break;
reached_target = true;
}
const HeapEntry top = heap.top();
heap.pop();
// The popped entry is the cheapest left, so exceeding the tolerance ends the run.
if (reached_target && top.cost > harvest_ceil)
break;
const int v1 = top.v1, v2 = top.v2;
if (!active[size_t(v1)] || !active[size_t(v2)])
continue;
if (version[size_t(v1)] != top.ver1 || version[size_t(v2)] != top.ver2)
continue;
if (shared_face_count(v1, v2) < 2)
continue;
lk_epoch += 2; // +2 so ep and ep+1 cannot collide with the next call
if (has_link_violation(v1, v2, lk_epoch))
continue;
if (check_flipped(v1, v2, top.p) || check_flipped(v2, v1, top.p))
continue;
// v1 survives at the new position, v2 goes.
pos[size_t(v1)] = top.p;
quadrics[size_t(v1)] += quadrics[size_t(v2)];
++version[size_t(v1)];
for (int s = vf_head[size_t(v2)]; s >= 0;) {
const size_t f = size_t(slot_face[size_t(s)]);
const int s_next = slot_next[size_t(s)]; // read before the list is modified
if (faces[f * 3] >= 0) {
for (int k = 0; k < 3; ++k)
if (faces[f * 3 + size_t(k)] == v2) {
faces[f * 3 + size_t(k)] = v1;
break;
}
const int fa = faces[f * 3], fb = faces[f * 3 + 1], fc = faces[f * 3 + 2];
if (fa == fb || fb == fc || fa == fc) {
for (int k = 0; k < 3; ++k)
if (const int sk = face_slot[f * 3 + size_t(k)]; sk >= 0) {
unlink_slot(sk);
face_slot[f * 3 + size_t(k)] = -1;
}
faces[f * 3] = faces[f * 3 + 1] = faces[f * 3 + 2] = -1;
--active_faces;
} else
move_slot(s, v1);
}
s = s_next;
}
active[size_t(v2)] = 0;
++epoch;
for (int sv = vf_head[size_t(v1)]; sv >= 0; sv = slot_next[size_t(sv)]) {
const size_t f = size_t(slot_face[size_t(sv)]);
if (faces[f * 3] < 0)
continue;
for (int k = 0; k < 3; ++k) {
const int nb = faces[f * 3 + size_t(k)];
if (nb == v1 || nb_stamp[size_t(nb)] == epoch)
continue;
nb_stamp[size_t(nb)] = epoch;
// v1 is never locked - a locked edge never entered the heap.
if (active[size_t(nb)] && (locked_vert.empty() || !locked_vert[size_t(nb)]))
push_edge(v1, nb);
}
}
if (on_progress) {
const double p = std::min(1.0, double(init_faces - active_faces) / double(to_remove));
if (p - last_progress > 0.005) {
last_progress = p;
if (!on_progress(p))
break;
}
}
}
// Rebuild from the surviving faces, with per-face normals.
TriSoup &out = result.geometry;
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
const Vec3f a = pos[size_t(faces[f * 3])].cast<float>();
const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast<float>();
const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast<float>();
const Vec3f nrm = (b - a).cross(c - a).normalized();
out.pos.insert(out.pos.end(), { a, b, c });
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,56 @@
#pragma once
// Quadric error metric decimation (Garland & Heckbert), with two additions that matter on a
// displaced mesh.
//
// Crease quadrics: an interior edge sharper than the threshold gets penalty planes at both endpoints,
// perpendicular to each adjacent face and through the edge, weighted so such edges collapse last or
// not at all. A texture's hard step keeps its geometry while the flat ground around it reduces.
//
// Flat-face harvesting: the loop keeps going past the triangle target while each collapse's error
// stays under an absolute bound, so flat faces that cost nothing to remove are not left behind.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Reject a collapse deviating more than about 78 degrees from the old face normal.
static constexpr double DECIMATE_FLIP_DOT = 0.2;
// Edges sharper than 60 degrees are treated as creases.
static constexpr double DECIMATE_CREASE_COS = 0.5;
// Quadric penalty weight for a crease plane.
static constexpr double DECIMATE_CREASE_WEIGHT = 1e4;
// Upper bound in mm on the deviation a harvested collapse may introduce; the real one is smaller,
// since the cost sums squared distances over all incident faces.
//
// Absolute, not relative to the cost at which the target was crossed. A relative band fails in the
// case with the most to shed: when the target is reached with a large flat surplus left, the crossing
// cost is essentially zero, so the band is too and nothing is harvested.
static constexpr double DECIMATE_DEFAULT_HARVEST_TOL = 0.005;
// Returns false to cancel.
using DecimateProgressFn = std::function<bool(double fraction)>;
struct DecimateResult
{
TriSoup geometry;
// The locked faces alone met the target, so it was unreachable without touching preserved
// geometry.
bool locked_over_budget = false;
};
// `locked_faces`: one entry per input triangle; a vertex touching one may neither move nor be
// removed, which also pins the ring between the two regions.
DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool harvest_flat = true,
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL,
const std::vector<uint8_t> &locked_faces = {},
const DecimateProgressFn &on_progress = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,268 @@
#include "TextureBakeDisplace.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
namespace Slic3r {
namespace TextureBake {
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
const DisplaceSettings &settings, const DisplaceBounds &bounds,
const DisplaceProgressFn &on_progress)
{
TriSoup out;
const size_t count = geometry.pos.size();
if (count == 0 || !sample)
return geometry;
out.pos.resize(count);
out.nrm.resize(count);
// Everything below is keyed by this id, which is what makes one vector per position expressible.
const bool need_id_positions = settings.boundary_falloff > 0.f;
QuantizedPointMap dedup(WELD_GRID_GEOMETRY, std::min(count, size_t(1) << 22));
std::vector<int> vertex_id(count);
std::vector<Vec3f> id_pos;
int next_id = 0;
for (size_t i = 0; i < count; ++i) {
const int id = dedup.get_or_set(geometry.pos[i], next_id);
if (dedup.inserted()) {
++next_id;
if (need_id_positions)
id_pos.push_back(geometry.pos[i]);
}
vertex_id[i] = id;
}
const size_t unique_count = size_t(next_id);
// Pass 1: area-weighted smooth normals per position, plus what masking and falloff need.
std::vector<Vec3d> smooth_nrm(unique_count, Vec3d::Zero());
std::vector<double> masked_area(unique_count, 0.0), total_area(unique_count, 0.0);
const bool have_weights = !geometry.exclude_weight.empty();
std::vector<uint8_t> user_excluded_face(have_weights ? count / 3 : 0, 0);
std::vector<uint8_t> excluded_pos(have_weights ? unique_count : 0, 0);
for (size_t t = 0; t + 2 < count; t += 3) {
const Vec3d a = geometry.pos[t].cast<double>();
const Vec3d face_n = (geometry.pos[t + 1].cast<double>() - a).cross(geometry.pos[t + 2].cast<double>() - a);
const double face_area = face_n.norm(); // twice the triangle area, so weighting is natural
const double nz = face_area > 1e-12 ? face_n.z() / face_area : 0.0;
const double face_angle = std::acos(std::min(1.0, std::abs(nz))) * (180.0 / M_PI);
const bool angle_masked =
nz < 0.0 ? (settings.bottom_angle_limit > 0.f && face_angle <= settings.bottom_angle_limit)
: (settings.top_angle_limit > 0.f && face_angle <= settings.top_angle_limit);
// Thresholded high, not at a half: merging by maximum leaves a face bordering an excluded one
// with two corners at 1.0, averaging about 0.67, which a half threshold would misread.
bool user_excluded = false;
if (have_weights) {
const float avg = (geometry.exclude_weight[t] + geometry.exclude_weight[t + 1] +
geometry.exclude_weight[t + 2]) / 3.f;
user_excluded = avg > 0.99f;
if (user_excluded)
user_excluded_face[t / 3] = 1;
}
for (int v = 0; v < 3; ++v) {
const size_t vid = size_t(vertex_id[t + size_t(v)]);
if (user_excluded && have_weights)
excluded_pos[vid] = 1;
// Subdivision split vertices at sharp edges, so these are smooth across soft edges and
// sharp across hard ones - no faceting on round surfaces, no rounding of corners.
smooth_nrm[vid] += geometry.nrm[t + size_t(v)].cast<double>() * face_area;
if (angle_masked)
masked_area[vid] += face_area;
total_area[vid] += face_area;
}
}
// The pre-normalisation magnitude over the total area says how much the neighbouring faces agree:
// near 1 they do, near 0 they cancelled, meaning a knife edge with no usable surface direction.
std::vector<double> reliability(unique_count, 0.0);
for (size_t id = 0; id < unique_count; ++id) {
const double len = smooth_nrm[id].norm();
reliability[id] = (len > 0.0 && total_area[id] > 0.0) ? len / total_area[id] : 0.0;
smooth_nrm[id] = (len > 0.0) ? Vec3d(smooth_nrm[id] / len) : Vec3d(0.0, 0.0, 1.0);
}
// Pass 1.5: the smoothed blend normal - see the header for why it is separate.
std::vector<Vec3d> blend_nrm = smooth_nrm;
if (settings.blend_normal_smoothing > 0 && unique_count > 0) {
// CSR adjacency over the welded graph, deliberately a multigraph: duplicates weight a pair by
// how often it shares an edge, so a well-connected surface couples more strongly.
std::vector<uint32_t> degree(unique_count, 0);
const auto add_degree = [&](int a, int b) {
if (a != b) { ++degree[size_t(a)]; ++degree[size_t(b)]; }
};
for (size_t t = 0; t + 2 < count; t += 3) {
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
add_degree(a, b); add_degree(b, c); add_degree(c, a);
}
std::vector<uint32_t> csr_start(unique_count + 1, 0);
for (size_t id = 0; id < unique_count; ++id)
csr_start[id + 1] = csr_start[id] + degree[id];
std::vector<uint32_t> neighbors(csr_start[unique_count]);
std::vector<uint32_t> cursor(unique_count, 0);
const auto add_edge = [&](int a, int b) {
if (a == b)
return;
neighbors[csr_start[size_t(a)] + cursor[size_t(a)]++] = uint32_t(b);
neighbors[csr_start[size_t(b)] + cursor[size_t(b)]++] = uint32_t(a);
};
for (size_t t = 0; t + 2 < count; t += 3) {
const int a = vertex_id[t], b = vertex_id[t + 1], c = vertex_id[t + 2];
add_edge(a, b); add_edge(b, c); add_edge(c, a);
}
std::vector<Vec3d> cur = smooth_nrm, nxt(unique_count, Vec3d::Zero());
for (int iter = 0; iter < settings.blend_normal_smoothing; ++iter) {
for (size_t id = 0; id < unique_count; ++id) {
const uint32_t s = csr_start[id], e = csr_start[id + 1];
if (e == s) {
nxt[id] = cur[id];
continue;
}
Vec3d sum = Vec3d::Zero();
for (uint32_t k = s; k < e; ++k)
sum += cur[neighbors[k]];
sum /= double(e - s);
const double len = sum.norm();
// Cancelling neighbours mean a knife edge; keep what we had.
nxt[id] = (len > 1e-12) ? Vec3d(sum / len) : cur[id];
}
cur.swap(nxt);
}
blend_nrm = std::move(cur);
}
// A boundary position borders both masked and unmasked faces, or sits on the exclusion seam.
// Every other position gets its distance to the nearest one, ramped to 1 at the falloff distance.
std::vector<double> falloff;
if (settings.boundary_falloff > 0.f && unique_count > 0) {
std::vector<Vec3f> boundary;
for (size_t id = 0; id < unique_count; ++id) {
const double frac = total_area[id] > 0.0 ? masked_area[id] / total_area[id] : 0.0;
const bool on_excl = !excluded_pos.empty() && excluded_pos[id] != 0;
if (on_excl || (frac > 0.0 && frac < 1.0))
boundary.push_back(id_pos[id]);
}
falloff.assign(unique_count, 1.0);
if (!boundary.empty()) {
// A uniform grid: the query is nearest-point only, so a tree costs more than it saves.
Vec3f lo = boundary.front(), hi = boundary.front();
for (const Vec3f &p : boundary) {
lo = lo.cwiseMin(p);
hi = hi.cwiseMax(p);
}
const Vec3f span = (hi - lo).cwiseMax(Vec3f(1e-6f, 1e-6f, 1e-6f));
const int res = std::clamp(int(std::ceil(std::cbrt(double(boundary.size())) * 2.0)), 4, 128);
const Vec3f cell = span / float(res);
const float cell_min = cell.minCoeff();
const auto cell_of = [&](const Vec3f &p) {
Vec3i32 c;
for (int k = 0; k < 3; ++k)
c[k] = std::clamp(int((p[k] - lo[k]) / span[k] * float(res)), 0, res - 1);
return c;
};
const auto cell_index = [&](int x, int y, int z) {
return size_t(z) * size_t(res) * size_t(res) + size_t(y) * size_t(res) + size_t(x);
};
std::vector<std::vector<int>> grid(size_t(res) * size_t(res) * size_t(res));
for (size_t i = 0; i < boundary.size(); ++i) {
const Vec3i32 c = cell_of(boundary[i]);
grid[cell_index(c.x(), c.y(), c.z())].push_back(int(i));
}
const double radius = double(settings.boundary_falloff);
for (size_t id = 0; id < unique_count; ++id) {
const Vec3f &p = id_pos[id];
const Vec3i32 c = cell_of(p);
double best = std::numeric_limits<double>::max();
// Anything in shell r is at least (r - 1) cells away, so once the best found is within
// that bound nothing closer can be hiding further out.
for (int r = 0; r < res; ++r) {
for (int dz = -r; dz <= r; ++dz)
for (int dy = -r; dy <= r; ++dy)
for (int dx = -r; dx <= r; ++dx) {
// The shell only; its interior was covered by a smaller r.
if (r > 0 && std::abs(dx) != r && std::abs(dy) != r && std::abs(dz) != r)
continue;
const int qx = c.x() + dx, qy = c.y() + dy, qz = c.z() + dz;
if (qx < 0 || qy < 0 || qz < 0 || qx >= res || qy >= res || qz >= res)
continue;
for (const int bi : grid[cell_index(qx, qy, qz)])
best = std::min(best, double((boundary[size_t(bi)] - p).norm()));
}
if (best <= double(r) * double(cell_min))
break;
}
falloff[id] = (best == std::numeric_limits<double>::max() || radius <= 0.0)
? 1.0
: std::clamp(best / radius, 0.0, 1.0);
}
}
}
// Pass 2: one sample per unique position.
std::vector<double> grey(unique_count, 0.0);
std::vector<uint8_t> grey_set(unique_count, 0);
for (size_t i = 0; i < count; ++i) {
const size_t vid = size_t(vertex_id[i]);
if (grey_set[vid])
continue;
grey_set[vid] = 1;
grey[vid] = double(sample(geometry.pos[i], smooth_nrm[vid].cast<float>(),
blend_nrm[vid].cast<float>()));
}
// Pass 3: move every copy of a position by the identical vector.
for (size_t i = 0; i < count; ++i) {
const Vec3f &p = geometry.pos[i];
const size_t vid = size_t(vertex_id[i]);
// Only angle masking uses the per-position blend, so an excluded face never dims its
// neighbours through a shared vertex.
const bool face_excluded = !user_excluded_face.empty() && user_excluded_face[i / 3] != 0;
// Pinned where an included face shares a position with an excluded one, sealing the boundary.
const bool sealed_boundary =
!face_excluded && !excluded_pos.empty() && excluded_pos[vid] != 0;
const double masked_frac = total_area[vid] > 0.0 ? masked_area[vid] / total_area[vid] : 0.0;
const double centered = settings.symmetric ? (grey[vid] - 0.5) : grey[vid];
const double ramp = falloff.empty() ? 1.0 : falloff[vid];
const double disp = (face_excluded || sealed_boundary)
? 0.0
: ramp * (1.0 - masked_frac) * centered * double(settings.amplitude);
Vec3d moved = p.cast<double>() + smooth_nrm[vid] * disp;
// Stop a partly masked vertex poking through the surface it borders.
if (masked_frac > 0.0) {
if (settings.bottom_angle_limit > 0.f && moved.z() < double(p.z())) moved.z() = double(p.z());
if (settings.top_angle_limit > 0.f && moved.z() > double(p.z())) moved.z() = double(p.z());
}
if (settings.no_downward_z && moved.z() < double(p.z()))
moved.z() = double(p.z());
// A vertex starting on the bottom plane stays there: otherwise a downward-facing face pulls
// *up* where the sample is below mid-grey, leaving bed-contact vertices at differing heights.
if (settings.no_downward_z && double(p.z()) <= double(bounds.min.z()) + 1e-5)
moved.z() = double(p.z());
out.pos[i] = moved.cast<float>();
if (on_progress && (i % 5000) == 0 && !on_progress(double(i) / double(count)))
return geometry; // cancelled: hand back the input untouched
}
// Per-face, not averaged across shared positions: averaging can flip an excluded face's normal
// when its neighbours moved outward.
for (size_t t = 0; t + 2 < count; t += 3) {
const Vec3f n = (out.pos[t + 1] - out.pos[t]).cross(out.pos[t + 2] - out.pos[t]).normalized();
out.nrm[t] = out.nrm[t + 1] = out.nrm[t + 2] = n;
}
out.exclude_weight = geometry.exclude_weight;
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,68 @@
#pragma once
// Displacement along surface normals.
//
// The mesh is non-indexed, so at a shared edge two triangles hold the same position with different
// face normals; displacing each copy along its own normal sends them to different points and opens a
// crack. So one smooth (area-weighted) normal per unique position drives both the sample lookup and
// the displacement direction, every copy moves by the same vector, and the result is watertight by
// construction. Displaced normals are then smooth at hard edges, but the geometry is still faceted,
// so printed edges stay sharp.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Height at a point, called once per unique welded position. `smooth_normal` is the vector the
// displacement will move along; `blend_normal` is that after smoothing, for projection blend weights.
using HeightSampleFn = std::function<float(const Vec3f &position, const Vec3f &smooth_normal,
const Vec3f &blend_normal)>;
struct DisplaceSettings
{
// Displacement height in mm, applied to the sampled value.
float amplitude = 0.4f;
// Sample around a mid-grey rest level rather than displacing outward only.
bool symmetric = false;
// Faces flatter than these (degrees from horizontal) are held back, leaving bed-contact and top
// surfaces alone. 0 disables that side.
float bottom_angle_limit = 5.f;
float top_angle_limit = 0.f;
// Never move a vertex below its original Z, so no new overhang. The sideways component is kept.
bool no_downward_z = false;
// Distance in mm over which displacement ramps up from a mask boundary. 0 leaves a hard edge.
float boundary_falloff = 0.f;
// Laplacian iterations on the blend normal only - the displacement direction must stay the exact
// smooth normal or copies of a position move differently and the mesh cracks. Inside a blend band
// the weight gradient is largest, so a few degrees of vertex-to-vertex jitter multiplies the
// difference between two unrelated height samples into visible seam noise. A no-op on an
// already-smooth surface.
int blend_normal_smoothing = 32;
};
// Model extents; only the minimum Z is read, for the bottom-plane clamp.
struct DisplaceBounds
{
Vec3f min = Vec3f::Zero();
Vec3f max = Vec3f::Zero();
};
// Returns false to cancel.
using DisplaceProgressFn = std::function<bool(double fraction)>;
TriSoup apply_displacement(const TriSoup &geometry, const HeightSampleFn &sample,
const DisplaceSettings &settings, const DisplaceBounds &bounds,
const DisplaceProgressFn &on_progress = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,25 @@
#include "TextureBakeIndex.hpp"
#include <algorithm>
namespace Slic3r {
namespace TextureBake {
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant)
{
WeldResult out;
QuantizedPointMap map(quant, std::min<size_t>(positions.size(), size_t(1) << 22));
out.vertex_id.resize(positions.size());
int next_id = 0;
for (size_t i = 0; i < positions.size(); ++i) {
const int id = map.get_or_set(positions[i], next_id);
if (map.inserted())
++next_id;
out.vertex_id[i] = id;
}
out.unique_count = next_id;
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,164 @@
#pragma once
// Vertex welding for the texture bake pipeline. The pipeline works on non-indexed triangle soup, so
// a shared point exists once per incident triangle with float noise between the copies; welding maps
// each quantised position to one integer id.
//
// The three grids below are deliberately not unified - changing one at a call site changes
// watertightness. 100 um matches the precision files are written with; 10 um keeps small fillet
// vertices distinct (they merge at 100 um, giving needle artifacts after displacement) while still
// absorbing float noise; 1 um is what collapse positioning needs.
#include <cmath>
#include <cstdint>
#include <vector>
#include "../Point.hpp"
namespace Slic3r {
namespace TextureBake {
static constexpr double WELD_GRID_EXPORT = 1e4; // 100 um
static constexpr double WELD_GRID_GEOMETRY = 1e5; // 10 um
static constexpr double WELD_GRID_DECIMATION = 1e6; // 1 um
// Round half toward positive infinity. Quantised coordinates hit exact halves often enough that the
// tie rule matters.
inline int64_t grid_round(double v) { return int64_t(std::floor(v + 0.5)); }
// Open-addressing table over flat arrays: no allocation per lookup, exact integer key comparison.
// Values must be non-negative; -1 is the empty sentinel and what get() returns on a miss.
class QuantizedPointMap
{
public:
explicit QuantizedPointMap(double quant, size_t expected = 256) : m_quant(quant)
{
size_t cap = 16;
const size_t target = std::max<size_t>(16, size_t(std::ceil(double(expected) / 0.6)));
while (cap < target)
cap *= 2;
alloc(cap);
}
size_t size() const { return m_size; }
// Whether the last get_or_set() inserted rather than found.
bool inserted() const { return m_inserted; }
int get(float x, float y, float z)
{
return m_val[slot(grid_round(double(x) * m_quant), grid_round(double(y) * m_quant),
grid_round(double(z) * m_quant))];
}
int get(const Vec3f &p) { return get(p.x(), p.y(), p.z()); }
// The value already stored for this position's grid cell; if there is none, store `value` and
// return it. inserted() then says which of the two happened.
int get_or_set(float x, float y, float z, int value)
{
const int64_t qx = grid_round(double(x) * m_quant);
const int64_t qy = grid_round(double(y) * m_quant);
const int64_t qz = grid_round(double(z) * m_quant);
const size_t i = slot(qx, qy, qz);
if (m_val[i] != -1) {
m_inserted = false;
return m_val[i];
}
m_qx[i] = qx; m_qy[i] = qy; m_qz[i] = qz;
m_val[i] = value;
m_inserted = true;
if (++m_size > size_t(double(m_cap) * 0.7))
grow();
return value;
}
int get_or_set(const Vec3f &p, int value) { return get_or_set(p.x(), p.y(), p.z(), value); }
// The same table as a set of integer tuples (edge marking, midpoint cache). Quantisation is
// bypassed: routing ids through the float overloads loses precision above 2^24.
int get_key(int64_t a, int64_t b, int64_t c) { return m_val[slot(a, b, c)]; }
int get_or_set_key(int64_t a, int64_t b, int64_t c, int value)
{
const size_t i = slot(a, b, c);
if (m_val[i] != -1) {
m_inserted = false;
return m_val[i];
}
m_qx[i] = a; m_qy[i] = b; m_qz[i] = c;
m_val[i] = value;
m_inserted = true;
if (++m_size > size_t(double(m_cap) * 0.7))
grow();
return value;
}
private:
void alloc(size_t cap)
{
m_cap = cap;
m_mask = cap - 1;
m_qx.assign(cap, 0);
m_qy.assign(cap, 0);
m_qz.assign(cap, 0);
m_val.assign(cap, -1);
}
size_t slot(int64_t qx, int64_t qy, int64_t qz) const
{
uint32_t h = uint32_t(int32_t(qx) * int32_t(0x9E3779B1)) ^
uint32_t(int32_t(qy) * int32_t(0x85EBCA77)) ^
uint32_t(int32_t(qz) * int32_t(0xC2B2AE3D));
h ^= h >> 15;
size_t i = size_t(h) & m_mask;
// Equality is checked against the stored 64-bit keys, so truncating to 32 bits for the hash
// costs collisions at worst, never a wrong answer.
while (m_val[i] != -1) {
if (m_qx[i] == qx && m_qy[i] == qy && m_qz[i] == qz)
return i;
i = (i + 1) & m_mask;
}
return i;
}
void grow()
{
std::vector<int64_t> oqx = std::move(m_qx), oqy = std::move(m_qy), oqz = std::move(m_qz);
std::vector<int> oval = std::move(m_val);
const size_t ocap = m_cap;
alloc(ocap * 2);
for (size_t i = 0; i < ocap; ++i) {
if (oval[i] == -1)
continue;
const size_t s = slot(oqx[i], oqy[i], oqz[i]);
m_qx[s] = oqx[i]; m_qy[s] = oqy[i]; m_qz[s] = oqz[i];
m_val[s] = oval[i];
}
}
double m_quant;
size_t m_cap = 0, m_mask = 0, m_size = 0;
bool m_inserted = false;
std::vector<int64_t> m_qx, m_qy, m_qz;
std::vector<int> m_val;
};
// Three consecutive entries per triangle. The indexers turn this into shared vertices where a stage
// needs adjacency.
struct TriSoup
{
std::vector<Vec3f> pos;
std::vector<Vec3f> nrm; // parallel to pos
std::vector<float> exclude_weight; // parallel to pos; empty when nothing is excluded
size_t triangle_count() const { return pos.size() / 3; }
bool empty() const { return pos.empty(); }
};
// Assign each vertex the sequential id of its quantised position, first occurrence winning.
struct WeldResult
{
std::vector<int> vertex_id;
int unique_count = 0;
};
WeldResult weld_vertices(const std::vector<Vec3f> &positions, double quant);
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,62 @@
#include "TextureBakeMesh.hpp"
#include <algorithm>
namespace Slic3r {
namespace TextureBake {
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded)
{
TriSoup out;
const size_t n = its.indices.size();
out.pos.resize(n * 3);
out.nrm.resize(n * 3);
const bool have_excl = face_excluded.size() == n;
if (have_excl)
out.exclude_weight.resize(n * 3);
for (size_t t = 0; t < n; ++t) {
const stl_triangle_vertex_indices &tri = its.indices[t];
const Vec3f a = its.vertices[size_t(tri[0])];
const Vec3f b = its.vertices[size_t(tri[1])];
const Vec3f c = its.vertices[size_t(tri[2])];
Vec3f nrm = (b - a).cross(c - a);
const float len = nrm.norm();
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
out.pos[t * 3] = a;
out.pos[t * 3 + 1] = b;
out.pos[t * 3 + 2] = c;
// Per-face on purpose: the accurate indexer derives smooth normals and splits at sharp edges
// itself, so averaged ones would pre-empt that.
out.nrm[t * 3] = out.nrm[t * 3 + 1] = out.nrm[t * 3 + 2] = nrm;
if (have_excl) {
const float w = face_excluded[t] ? 1.f : 0.f;
out.exclude_weight[t * 3] = out.exclude_weight[t * 3 + 1] = out.exclude_weight[t * 3 + 2] = w;
}
}
return out;
}
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
{
indexed_triangle_set out;
const size_t n = soup.pos.size();
out.indices.reserve(n / 3);
QuantizedPointMap map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
for (size_t i = 0; i < n; ++i) {
id[i] = map.get_or_set(soup.pos[i], int(out.vertices.size()));
if (map.inserted())
out.vertices.push_back(soup.pos[i]);
}
for (size_t t = 0; t + 2 < n; t += 3) {
// Welded-together corners carry no area.
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
continue;
out.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
}
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,19 @@
#pragma once
// Conversion between the pipeline's triangle soup and the indexed mesh used elsewhere. The pipeline
// stays on soup because each stage welds on its own grid, and those differences are load-bearing.
#include "TextureBakeIndex.hpp"
#include "../TriangleMesh.hpp"
namespace Slic3r {
namespace TextureBake {
// `face_excluded`: one entry per input triangle, becoming the soup's per-corner exclusion weight.
TriSoup to_soup(const indexed_triangle_set &its, const std::vector<uint8_t> &face_excluded = {});
// Welds at the geometry grid.
indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup);
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,216 @@
#include "TextureBakePipeline.hpp"
#include <algorithm>
#include <cmath>
namespace Slic3r {
namespace TextureBake {
void clamp_below_bottom(TriSoup &geometry, float bottom_z)
{
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
bool dirty = false;
for (int k = 0; k < 3; ++k)
if (geometry.pos[t + size_t(k)].z() < bottom_z) {
geometry.pos[t + size_t(k)].z() = bottom_z;
dirty = true;
}
if (!dirty)
continue;
Vec3f n = (geometry.pos[t + 1] - geometry.pos[t]).cross(geometry.pos[t + 2] - geometry.pos[t]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
geometry.nrm[t] = geometry.nrm[t + 1] = geometry.nrm[t + 2] = n;
}
}
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol)
{
const size_t vert_count = geometry.pos.size();
const size_t tri_count = vert_count / 3;
if (tri_count == 0 || tol <= 0.0)
return 0;
// Weld at the finest grid: by this point copies of one position are bit-identical, because every
// earlier stage moved them by the same vector.
QuantizedPointMap weld(WELD_GRID_DECIMATION, std::min(vert_count, size_t(1) << 22));
std::vector<int> vid(vert_count);
int unique = 0;
for (size_t i = 0; i < vert_count; ++i) {
vid[i] = weld.get_or_set(geometry.pos[i], unique);
if (weld.inserted())
++unique;
}
// Incident corners per position, CSR style.
std::vector<uint32_t> start(size_t(unique) + 1, 0);
for (size_t i = 0; i < vert_count; ++i)
++start[size_t(vid[i]) + 1];
for (size_t id = 0; id < size_t(unique); ++id)
start[id + 1] += start[id];
std::vector<uint32_t> inc(vert_count), cursor(size_t(unique), 0);
for (size_t i = 0; i < vert_count; ++i)
inc[start[size_t(vid[i])] + cursor[size_t(vid[i])]++] = uint32_t(i);
const double fold_cos = std::cos(75.0 * M_PI / 180.0);
std::vector<uint8_t> dirty_tri(tri_count, 0);
for (size_t id = 0; id < size_t(unique); ++id) {
const float z = geometry.pos[inc[start[id]]].z();
if (z == bottom_z || std::abs(double(z) - double(bottom_z)) > tol)
continue;
// Simulate the move: every incident triangle must keep positive area and must not fold.
bool ok = true;
for (uint32_t k = start[id]; k < start[id + 1] && ok; ++k) {
const size_t t = size_t(inc[k]) / 3;
Vec3f p[3];
for (int v = 0; v < 3; ++v) {
p[v] = geometry.pos[t * 3 + size_t(v)];
if (vid[t * 3 + size_t(v)] == int(id))
p[v].z() = bottom_z;
}
const Vec3d on = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]).cast<double>();
const Vec3d nn = (p[1] - p[0]).cross(p[2] - p[0]).cast<double>();
const double o2 = on.squaredNorm(), n2 = nn.squaredNorm();
if (n2 < 1e-20) { ok = false; break; } // would collapse to zero area
if (o2 < 1e-20) continue; // already degenerate, cannot judge a rotation
const double dot = on.dot(nn);
if (dot < 0.0 || dot * dot < fold_cos * fold_cos * o2 * n2)
ok = false;
}
if (!ok)
continue;
for (uint32_t k = start[id]; k < start[id + 1]; ++k) {
geometry.pos[inc[k]].z() = bottom_z;
dirty_tri[size_t(inc[k]) / 3] = 1;
}
}
size_t dirty = 0;
for (size_t t = 0; t < tri_count; ++t) {
if (!dirty_tri[t])
continue;
++dirty;
Vec3f n = (geometry.pos[t * 3 + 1] - geometry.pos[t * 3])
.cross(geometry.pos[t * 3 + 2] - geometry.pos[t * 3]);
const float len = n.norm();
n = (len > 0.f) ? Vec3f(n / len) : Vec3f(0.f, 0.f, 1.f);
geometry.nrm[t * 3] = geometry.nrm[t * 3 + 1] = geometry.nrm[t * 3 + 2] = n;
}
return dirty;
}
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
const PipelineSettings &settings, const DisplaceBounds &bounds,
PipelineMode mode, const std::vector<uint8_t> &face_excluded,
const PipelineProgressFn &on_progress)
{
PipelineResult result;
const auto report = [&](const char *stage, double f) {
return !on_progress || on_progress(stage, f);
};
if (input.empty() || !sample) {
result.geometry = input;
return result;
}
// 1. Refine to the target edge length.
SubdivideResult sub = subdivide(
input, settings.refine_length, face_excluded, /* fast */ false, settings.safety_cap,
[&](double f, size_t, double) { return report("subdivide", f); });
result.safety_cap_hit = sub.safety_cap_hit;
if (!report("subdivide", 1.0)) {
result.canceled = true;
return result;
}
// 2. Dissolve the slivers refinement inherited, then recover the edges that lengthened.
if (settings.regularize) {
RegularizeOptions ropts = settings.regularize_opts;
ropts.preserve_excluded = settings.preserve_untextured;
RegularizeResult reg = regularize_mesh(sub.geometry, sub.face_parent_id,
settings.refine_length, ropts);
result.collapse_count = reg.collapse_count;
if (!report("regularize", 1.0)) {
result.canceled = true;
return result;
}
if (reg.collapse_count > 0) {
// Excluded faces are carried on the soup itself, so the flag is re-derived rather than
// indexed across the collapse.
std::vector<uint8_t> excl;
if (!reg.geometry.exclude_weight.empty()) {
excl.assign(reg.geometry.triangle_count(), 0);
for (size_t t = 0; t < excl.size(); ++t)
excl[t] = reg.geometry.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
sub = subdivide(reg.geometry, settings.refine_length * settings.regularize_second_pass_mul,
excl, false, settings.safety_cap,
[&](double f, size_t, double) { return report("re-subdivide", f); });
result.safety_cap_hit = result.safety_cap_hit || sub.safety_cap_hit;
// The second pass renumbers faces, so the parent map has to be composed through it.
std::vector<int> composed(sub.face_parent_id.size());
for (size_t i = 0; i < composed.size(); ++i) {
const int mid = sub.face_parent_id[i];
composed[i] = (mid >= 0 && size_t(mid) < reg.face_parent_id.size())
? reg.face_parent_id[size_t(mid)] : -1;
}
sub.face_parent_id = std::move(composed);
} else {
sub.geometry = std::move(reg.geometry);
sub.face_parent_id = std::move(reg.face_parent_id);
}
}
// 3. Displace.
TriSoup displaced = apply_displacement(sub.geometry, sample, settings.displace, bounds,
[&](double f) { return report("displace", f); });
if (!report("displace", 1.0)) {
result.canceled = true;
return result;
}
// 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys.
std::vector<int> parent = std::move(sub.face_parent_id);
if (mode == PipelineMode::Export) {
const bool needs_decimation = displaced.triangle_count() > settings.max_triangles;
if (needs_decimation || settings.harvest_flat) {
std::vector<uint8_t> locked;
if (settings.preserve_untextured && !displaced.exclude_weight.empty()) {
locked.assign(displaced.triangle_count(), 0);
for (size_t t = 0; t < locked.size(); ++t)
locked[t] = displaced.exclude_weight[t * 3] > 0.99f ? 1 : 0;
}
DecimateResult dec = decimate(displaced, settings.max_triangles, settings.harvest_flat,
settings.harvest_tol, locked,
[&](double f) { return report("decimate", f); });
result.locked_over_budget = dec.locked_over_budget;
displaced = std::move(dec.geometry);
parent.clear(); // no longer meaningful
}
if (!report("decimate", 1.0)) {
result.canceled = true;
return result;
}
}
// 5. Flatten the bed-contact surface.
if (settings.displace.bottom_angle_limit > 0.f)
clamp_below_bottom(displaced, bounds.min.z());
if (settings.bottom_snap_tol > 0.0)
snap_bottom_to_flat(displaced, bounds.min.z(), settings.bottom_snap_tol);
// 6. Close the T-junctions decimation left behind. Only meaningful when it ran.
if (mode == PipelineMode::Export && parent.empty())
displaced = resolve_t_junctions(displaced);
result.geometry = std::move(displaced);
result.face_parent_id = std::move(parent);
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,98 @@
#pragma once
// The bake pipeline:
//
// subdivide -> [regularize -> re-subdivide] -> displace -> [decimate]
// -> bottom clamp -> bottom snap -> [resolve T-junctions]
//
// Regularization sits between two subdivisions on purpose: it dissolves the slivers refinement
// inherited, which lengthens some edges past the target, and the second pass brings those back.
// Before any subdivision it would have nothing to work on, since the slivers come from refining a
// needle; after a single pass it would leave the mesh coarser than asked for.
//
// Decimation and repair are export-only - decimation drops the output-to-input face mapping a bake
// needs to carry per-face data forward.
#include <cstdint>
#include <functional>
#include <vector>
#include "TextureBakeDecimate.hpp"
#include "TextureBakeDisplace.hpp"
#include "TextureBakeIndex.hpp"
#include "TextureBakeRegularize.hpp"
#include "TextureBakeRepair.hpp"
#include "TextureBakeSubdivide.hpp"
namespace Slic3r {
namespace TextureBake {
enum class PipelineMode
{
// Keeps the face-parent mapping; skips decimation and repair.
Bake,
// The full sequence, including decimation and repair.
Export,
};
struct PipelineSettings
{
// Target edge length for the refinement, in mm.
double refine_length = 1.0;
// Sliver removal between the two subdivision passes.
bool regularize = true;
RegularizeOptions regularize_opts;
// Slightly above the first pass, so it recovers the edges regularization lengthened instead of
// re-refining what it just merged.
double regularize_second_pass_mul = 1.1;
DisplaceSettings displace;
// Export mode only.
size_t max_triangles = 750'000;
bool harvest_flat = true;
double harvest_tol = DECIMATE_DEFAULT_HARVEST_TOL;
// Lock the untextured region against both regularization and decimation.
bool preserve_untextured = true;
// Snap vertices within this of the bottom plane onto it. 0 disables.
double bottom_snap_tol = 0.1;
int safety_cap = SUBDIVIDE_SAFETY_CAP;
};
// Stage name and a fraction within it. Returning false cancels the run.
using PipelineProgressFn = std::function<bool(const char *stage, double fraction)>;
struct PipelineResult
{
TriSoup geometry;
// Output face -> input face. Empty in Export mode, where decimation invalidates it.
std::vector<int> face_parent_id;
bool safety_cap_hit = false;
bool locked_over_budget = false;
size_t collapse_count = 0;
bool canceled = false;
};
PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
const PipelineSettings &settings, const DisplaceBounds &bounds,
PipelineMode mode, const std::vector<uint8_t> &face_excluded = {},
const PipelineProgressFn &on_progress = {});
// Snap anything that ended below the model's original bottom back up to it.
void clamp_below_bottom(TriSoup &geometry, float bottom_z);
// Flatten the bed-contact surface by snapping positions within `tol` of the bottom plane onto it.
//
// Gated, not unconditional: an unconditional band snap also flattens the undersides of texture bumps
// near the base, folding them coplanar into the bottom face. Folded faces overlap the plate, so edges
// there pick up four incident faces - non-manifold edges and phantom shells on re-import. All copies
// of a position move together, and the move is rejected if any incident triangle would go degenerate
// or rotate more than about 75 degrees. A real bed-contact sliver rotates by a fraction of a degree
// and still snaps. Returns how many triangles moved.
size_t snap_bottom_to_flat(TriSoup &geometry, float bottom_z, double tol = 0.1);
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,380 @@
#include "TextureBakeRegularize.hpp"
#include <algorithm>
#include <cmath>
#include <limits>
#include <numeric>
namespace Slic3r {
namespace TextureBake {
namespace {
// Vertex-to-triangle lists as intrusive doubly linked lists of corner slots over flat arrays. Slot s
// is corner (triangle * 3 + k), owned by corners[s]. Deleted and moved corners are unlinked, so a
// collapse costs no allocation.
struct SlotLists
{
std::vector<int> head, next, prev;
void init(size_t vertex_count, size_t slot_count)
{
head.assign(vertex_count, -1);
next.assign(slot_count, -1);
prev.assign(slot_count, -1);
}
void link(int s, const std::vector<int> &corners)
{
const int v = corners[size_t(s)];
const int h = head[size_t(v)];
prev[size_t(s)] = -1;
next[size_t(s)] = h;
if (h != -1)
prev[size_t(h)] = s;
head[size_t(v)] = s;
}
void unlink(int s, const std::vector<int> &corners)
{
const int p = prev[size_t(s)], n = next[size_t(s)];
if (p != -1) next[size_t(p)] = n;
else head[size_t(corners[size_t(s)])] = n;
if (n != -1) prev[size_t(n)] = p;
}
};
} // namespace
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
double max_edge_length, const RegularizeOptions &opts)
{
RegularizeResult result;
const size_t tri_count = geometry.triangle_count();
if (tri_count == 0 || max_edge_length <= 0.0) {
result.geometry = geometry;
result.face_parent_id = face_parent_id;
return result;
}
const double base_max_len_sq = (max_edge_length * opts.slack) * (max_edge_length * opts.slack);
const double aggr_max_len_sq =
(max_edge_length * opts.aggressive_slack) * (max_edge_length * opts.aggressive_slack);
const double extreme_aspect2 = opts.extreme_sliver_aspect * opts.extreme_sliver_aspect;
const double aspect_thr2 = opts.aspect_threshold * opts.aspect_threshold;
// Double precision: a collapse writes a midpoint back and later collapses read it, so rounding
// would accumulate.
QuantizedPointMap pos_map(WELD_GRID_GEOMETRY, std::min(tri_count * 3, size_t(1) << 22));
std::vector<Vec3d> vert;
std::vector<int> corners(tri_count * 3);
vert.reserve(tri_count);
for (size_t i = 0; i < tri_count * 3; ++i) {
const Vec3f &p = geometry.pos[i];
const int id = pos_map.get_or_set(p, int(vert.size()));
if (pos_map.inserted())
vert.push_back(p.cast<double>());
corners[i] = id;
}
const size_t vert_count = vert.size();
std::vector<Vec3d> tri_nrm(tri_count, Vec3d::Zero());
std::vector<uint8_t> tri_deleted(tri_count, 0);
result.face_parent_id = face_parent_id;
if (result.face_parent_id.size() != tri_count)
result.face_parent_id.assign(tri_count, 0);
const auto sq_dist = [&](int a, int b) { return (vert[size_t(a)] - vert[size_t(b)]).squaredNorm(); };
const auto recompute_face_normal = [&](size_t t) {
const Vec3d &a = vert[size_t(corners[t * 3])];
const Vec3d n = (vert[size_t(corners[t * 3 + 1])] - a).cross(vert[size_t(corners[t * 3 + 2])] - a);
const double len = n.norm();
tri_nrm[t] = (len > 0.0) ? Vec3d(n / len) : Vec3d::Zero();
};
for (size_t t = 0; t < tri_count; ++t)
recompute_face_normal(t);
// Never updated - the normal gate measures against these, so drift cannot compound across rounds.
const std::vector<Vec3d> orig_nrm = tri_nrm;
// Squared thinness, the longest edge over the shortest altitude:
// thinness = lmax / hmin = lmax^2 / (2 * area), so thinness^2 = lmax^4 / |AB x AC|^2
//
// Not lmax/lmin, which misses what matters here: three near-collinear points can have all edges
// similar, so an edge ratio reports about 2 and the gate skips a triangle with near-zero area
// whose corners sample three unrelated texels. An equilateral scores about 1.15.
const auto tri_aspect_sq = [&](size_t t) -> double {
const Vec3d &a = vert[size_t(corners[t * 3])];
const Vec3d ab = vert[size_t(corners[t * 3 + 1])] - a;
const Vec3d ac = vert[size_t(corners[t * 3 + 2])] - a;
const Vec3d bc = vert[size_t(corners[t * 3 + 2])] - vert[size_t(corners[t * 3 + 1])];
const double lmax2 = std::max({ ab.squaredNorm(), ac.squaredNorm(), bc.squaredNorm() });
const double cross2 = ab.cross(ac).squaredNorm();
return cross2 > 0.0 ? lmax2 * lmax2 / cross2 : std::numeric_limits<double>::infinity();
};
SlotLists slots;
slots.init(vert_count, tri_count * 3);
for (size_t s = 0; s < tri_count * 3; ++s)
slots.link(int(s), corners);
// O(1) membership without clearing a set per collapse.
std::vector<uint32_t> vert_stamp(vert_count, 0), tri_stamp(tri_count, 0);
uint32_t stamp_gen = 0;
// Both endpoints of a hard edge are barred from being collapse endpoints, preserving such
// corners exactly while leaving flat-face interiors free.
//
// Skipped when either triangle is an extreme sliver: a sliver's normal is dominated by where its
// far apex sits, so noise pivots it tens of degrees with no feature behind it, and freezing on
// that would lock the very chains this pass exists to dissolve. Genuine features are bordered by
// well-shaped triangles and are unaffected.
std::vector<uint8_t> frozen_vert(vert_count, 0);
{
std::vector<double> tri_thin2(tri_count);
for (size_t t = 0; t < tri_count; ++t)
tri_thin2[t] = tri_aspect_sq(t);
QuantizedPointMap edge_seen(1.0, std::min(tri_count * 3, size_t(1) << 22));
for (size_t t = 0; t < tri_count; ++t)
for (int e = 0; e < 3; ++e) {
const int u = corners[t * 3 + size_t(e)];
const int v = corners[t * 3 + size_t((e + 1) % 3)];
const int lo = std::min(u, v), hi = std::max(u, v);
const int other = edge_seen.get_or_set_key(lo, hi, 0, int(t));
if (edge_seen.inserted())
continue;
if (tri_thin2[t] > extreme_aspect2 || tri_thin2[size_t(other)] > extreme_aspect2)
continue;
if (tri_nrm[t].dot(tri_nrm[size_t(other)]) < opts.sharp_edge_cos) {
frozen_vert[size_t(u)] = 1;
frozen_vert[size_t(v)] = 1;
}
}
}
// Exclusion freeze. The weight is constant across a face's corners, so the first one answers.
if (opts.preserve_excluded && !geometry.exclude_weight.empty())
for (size_t t = 0; t < tri_count; ++t)
if (geometry.exclude_weight[t * 3] > 0.99f)
for (int k = 0; k < 3; ++k)
frozen_vert[size_t(corners[t * 3 + size_t(k)])] = 1;
std::vector<int> wing_scratch, affected_scratch;
const auto third_vertex = [&](size_t t, int u, int v) {
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
if (a != u && a != v) return a;
if (b != u && b != v) return b;
return c;
};
const auto triangles_sharing_edge = [&](int u, int v) -> std::vector<int> & {
wing_scratch.clear();
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
if (corners[t * 3] == v || corners[t * 3 + 1] == v || corners[t * 3 + 2] == v)
wing_scratch.push_back(int(t));
}
return wing_scratch;
};
RegularizeRejectStats &stats = result.reject_stats;
const auto try_collapse = [&](int u, int v) -> bool {
if (u == v)
return false;
if (frozen_vert[size_t(u)] || frozen_vert[size_t(v)]) { ++stats.frozen; return false; }
// Two wings means a manifold interior edge.
std::vector<int> &wings = triangles_sharing_edge(u, v);
if (wings.size() != 2) { ++stats.wing_count; return false; }
const size_t w0 = size_t(wings[0]), w1 = size_t(wings[1]);
const int apex1 = third_vertex(w0, u, v), apex2 = third_vertex(w1, u, v);
if (apex1 == apex2) { ++stats.folded_apex; return false; }
// The edge cap loosens if *either* wing is extreme, since the re-subdivision recovers an
// over-long edge. The normal cap needs *both*, which is what protects fillets.
const double w1a = tri_aspect_sq(w0), w2a = tri_aspect_sq(w1);
const bool either_extreme = w1a > extreme_aspect2 || w2a > extreme_aspect2;
const bool both_extreme = w1a > extreme_aspect2 && w2a > extreme_aspect2;
const double eff_max_len_sq = either_extreme ? aggr_max_len_sq : base_max_len_sq;
const double eff_normal_cos =
both_extreme ? opts.aggressive_normal_delta_cos : opts.max_normal_delta_cos;
// A vertex sharing a triangle with both endpoints, other than the wing apexes, would go
// non-manifold. Stamp one side's neighbours, scan the other against them.
++stamp_gen;
for (int s = slots.head[size_t(v)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k)
if (const int x = corners[t * 3 + size_t(k)]; x != v)
vert_stamp[size_t(x)] = stamp_gen;
}
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k) {
const int x = corners[t * 3 + size_t(k)];
if (x != u && x != v && x != apex1 && x != apex2 && vert_stamp[size_t(x)] == stamp_gen) {
++stats.link_condition;
return false;
}
}
}
const Vec3d m = (vert[size_t(u)] + vert[size_t(v)]) * 0.5;
// Everything using either endpoint; the wings are being deleted.
++stamp_gen;
affected_scratch.clear();
for (const int endpoint : { u, v })
for (int s = slots.head[size_t(endpoint)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (tri_deleted[t] || t == w0 || t == w1)
continue;
if (tri_stamp[t] != stamp_gen) {
tri_stamp[t] = stamp_gen;
affected_scratch.push_back(int(t));
}
}
// Validate every affected triangle before touching anything.
for (const int ti : affected_scratch) {
const size_t t = size_t(ti);
Vec3d p[3];
for (int k = 0; k < 3; ++k) {
const int x = corners[t * 3 + size_t(k)];
p[k] = (x == u || x == v) ? m : vert[size_t(x)];
}
const double ab2 = (p[1] - p[0]).squaredNorm();
const double bc2 = (p[2] - p[1]).squaredNorm();
const double ca2 = (p[0] - p[2]).squaredNorm();
if (ab2 > eff_max_len_sq || bc2 > eff_max_len_sq || ca2 > eff_max_len_sq) {
++stats.edge_cap;
return false;
}
const Vec3d n = (p[1] - p[0]).cross(p[2] - p[0]);
const double nlen = n.norm();
if (nlen <= 0.0) { ++stats.degenerate; return false; }
if ((n / nlen).dot(orig_nrm[t]) < eff_normal_cos) { ++stats.normal_change; return false; }
}
// Apply: move u to the merged position and redirect every reference to v.
vert[size_t(u)] = m;
for (const size_t w : { w0, w1 }) {
tri_deleted[w] = 1;
for (int k = 0; k < 3; ++k)
slots.unlink(int(w * 3) + k, corners);
}
// A non-wing triangle contains v exactly once, so moving its slots suffices.
for (int s = slots.head[size_t(v)]; s != -1;) {
const int ns = slots.next[size_t(s)];
slots.unlink(s, corners);
corners[size_t(s)] = u;
slots.link(s, corners);
recompute_face_normal(size_t(s) / 3);
s = ns;
}
for (int s = slots.head[size_t(u)]; s != -1; s = slots.next[size_t(s)]) {
const size_t t = size_t(s) / 3;
if (!tri_deleted[t])
recompute_face_normal(t);
}
return true;
};
for (int round = 0; round < opts.maxrounds; ++round) {
// Rebuilt each round so earlier collapses inform the priorities.
std::vector<int> cand;
std::vector<double> cand_aspect;
for (size_t t = 0; t < tri_count; ++t) {
if (tri_deleted[t])
continue;
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
if (std::min({ sq_dist(a, b), sq_dist(b, c), sq_dist(c, a) }) <= 0.0)
continue;
const double aspect2 = tri_aspect_sq(t);
if (aspect2 < aspect_thr2)
continue;
cand.push_back(int(t));
cand_aspect.push_back(aspect2);
}
// Worst first; ties keep ascending order so the pass is deterministic.
std::vector<int> order(cand.size());
std::iota(order.begin(), order.end(), 0);
std::stable_sort(order.begin(), order.end(),
[&](int x, int y) { return cand_aspect[size_t(x)] > cand_aspect[size_t(y)]; });
size_t round_collapses = 0;
for (const int oi : order) {
const size_t t = size_t(cand[size_t(oi)]);
if (tri_deleted[t])
continue;
const int a = corners[t * 3], b = corners[t * 3 + 1], c = corners[t * 3 + 2];
// All three edges, shortest first: a sliver straddling a seam has its shortest edge
// crossing it, which the normal gate refuses, while a long edge along one surface
// collapses safely. Trying only the shortest would leave those stuck.
struct Cand { double len2; int u, v; };
Cand e[3] = { { sq_dist(a, b), a, b }, { sq_dist(b, c), b, c }, { sq_dist(c, a), c, a } };
std::stable_sort(std::begin(e), std::end(e),
[](const Cand &x, const Cand &y) { return x.len2 < y.len2; });
if (try_collapse(e[0].u, e[0].v) || try_collapse(e[1].u, e[1].v) ||
try_collapse(e[2].u, e[2].v))
++round_collapses;
}
result.collapse_count += round_collapses;
if (round_collapses == 0)
break;
}
// Drop deleted triangles and rebuild the soup.
const bool have_weights = !geometry.exclude_weight.empty();
std::vector<int> out_parent;
TriSoup &out = result.geometry;
for (size_t t = 0; t < tri_count; ++t) {
if (tri_deleted[t])
continue;
for (int k = 0; k < 3; ++k)
out.pos.push_back(vert[size_t(corners[t * 3 + size_t(k)])].cast<float>());
if (have_weights) {
// Constant across a face's corners.
const float w = geometry.exclude_weight[t * 3];
out.exclude_weight.insert(out.exclude_weight.end(), { w, w, w });
}
out_parent.push_back(result.face_parent_id[t]);
}
result.face_parent_id = std::move(out_parent);
// Rebuilt from the compacted geometry - the collapses moved vertices.
out.nrm.assign(out.pos.size(), Vec3f::Zero());
{
std::vector<Vec3d> accum(out.pos.size(), Vec3d::Zero());
QuantizedPointMap weld(WELD_GRID_GEOMETRY, out.pos.size());
std::vector<int> vid(out.pos.size());
int next = 0;
for (size_t i = 0; i < out.pos.size(); ++i) {
vid[i] = weld.get_or_set(out.pos[i], next);
if (weld.inserted())
++next;
}
std::vector<Vec3d> vn(size_t(next), Vec3d::Zero());
for (size_t t = 0; t * 3 < out.pos.size(); ++t) {
const Vec3d a = out.pos[t * 3].cast<double>();
const Vec3d n = (out.pos[t * 3 + 1].cast<double>() - a).cross(out.pos[t * 3 + 2].cast<double>() - a);
for (int k = 0; k < 3; ++k)
vn[size_t(vid[t * 3 + size_t(k)])] += n;
}
for (size_t i = 0; i < out.pos.size(); ++i) {
const Vec3d &n = vn[size_t(vid[i])];
const double l = n.norm();
out.nrm[i] = (l > 0.0) ? Vec3d(n / l).cast<float>() : Vec3f(0.f, 0.f, 1.f);
}
}
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,73 @@
#pragma once
// Sliver removal by short-edge collapse.
//
// Subdivision turns tessellation needles into chains of slivers that are within the edge-length
// budget but still poor triangles. A sliver's three vertices land on three unrelated texels, so the
// relief picks up noise that is an artifact of the tessellation rather than of the image.
//
// A candidate's edge is collapsed to its midpoint only if it passes three gates: no affected
// triangle may exceed the target edge times a slack factor; every affected triangle must keep its
// face normal within a bound of its *original* direction (which is what stops curved surfaces being
// flattened); and the link condition must hold, or the result would be non-manifold. Boundary and
// non-manifold edges are skipped outright. Rounds repeat until one achieves nothing.
#include <cstdint>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct RegularizeOptions
{
// Candidate threshold. Set to catch real slivers - chains measure in the hundreds - without
// sweeping up moderate fillet triangles, which sit between 2 and 5.
double aspect_threshold = 5.0;
// The base tier is loose on purpose: non-sliver boundary collapses must keep succeeding, since
// those give a chain the room to dissolve. A tight base leaves chains worse than before. The
// aggressive tier applies when at least one wing is an extreme sliver.
double slack = 3.0;
double aggressive_slack = 8.0;
// Thinness above which a wing counts as extreme: longest edge over shortest altitude.
double extreme_sliver_aspect = 8.0;
// Measured against each triangle's normal from before any collapse ran, so rounds of small
// allowed drift cannot compound into corner damage. Asymmetric two-tier: the loose bound needs
// *both* wings extreme, which matches a needle chain on a curved face but not a sliver beside a
// fillet, so fillets keep the tight bound.
double max_normal_delta_cos = 0.965925826289; // cos(15 degrees)
double aggressive_normal_delta_cos = 0.906307787037; // cos(25 degrees)
// Vertices on edges sharper than this are frozen, so hard features keep every original vertex.
double sharp_edge_cos = 0.866025403784; // cos(30 degrees)
int maxrounds = 8;
// Freeze excluded faces entirely, so untextured geometry is never modified.
bool preserve_excluded = false;
};
// Which gate blocked a collapse - the only practical way to tell why a region failed to merge.
struct RegularizeRejectStats
{
size_t frozen = 0, wing_count = 0, link_condition = 0, edge_cap = 0, normal_change = 0,
degenerate = 0, folded_apex = 0;
};
struct RegularizeResult
{
TriSoup geometry;
std::vector<int> face_parent_id;
size_t collapse_count = 0;
RegularizeRejectStats reject_stats;
};
RegularizeResult regularize_mesh(const TriSoup &geometry, const std::vector<int> &face_parent_id,
double max_edge_length, const RegularizeOptions &opts = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,215 @@
#include "TextureBakeRepair.hpp"
#include <algorithm>
#include <array>
#include <cmath>
#include <unordered_map>
#include <unordered_set>
namespace Slic3r {
namespace TextureBake {
namespace {
inline uint64_t edge_key(int a, int b)
{
const uint32_t lo = uint32_t(std::min(a, b)), hi = uint32_t(std::max(a, b));
return (uint64_t(lo) << 32) | uint64_t(hi);
}
// On the export grid a squared cross product is either 0 (collinear) or at least about 1e-16, the
// smallest real triangle being one grid unit per leg, so this separates the two cleanly.
constexpr double DEGENERATE_AREA_SQ = 1e-18;
} // namespace
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant)
{
EdgeDefects out;
const size_t n = geometry.pos.size();
out.triangles = n / 3;
QuantizedPointMap vmap(quant, std::min(n, size_t(1) << 22));
std::vector<int> id(n);
int next = 0;
for (size_t i = 0; i < n; ++i) {
id[i] = vmap.get_or_set(geometry.pos[i], next);
if (vmap.inserted())
++next;
}
std::unordered_map<uint64_t, int> counts;
for (size_t t = 0; t + 2 < n; t += 3) {
const int a = id[t], b = id[t + 1], c = id[t + 2];
if (a == b || b == c || a == c)
continue;
const int tri[3] = { a, b, c };
for (int e = 0; e < 3; ++e)
++counts[edge_key(tri[e], tri[(e + 1) % 3])];
}
for (const auto &[key, c] : counts) {
(void) key;
if (c == 1) ++out.open;
else if (c > 2) ++out.non_manifold;
}
return out;
}
size_t count_area_slivers(const TriSoup &geometry)
{
size_t n = 0;
for (size_t t = 0; t + 2 < geometry.pos.size(); t += 3) {
const Vec3d u = (geometry.pos[t + 1] - geometry.pos[t]).cast<double>();
const Vec3d v = (geometry.pos[t + 2] - geometry.pos[t]).cast<double>();
// The threshold a slicer applies: area below 1e-12 mm^2.
if (u.cross(v).squaredNorm() < 1e-24)
++n;
}
return n;
}
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
{
const size_t n_tri = geometry.triangle_count();
const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol;
const double Q = opts.weld_quant;
// Snapped, not just welded: keeping unrounded coordinates lets a thin triangle pass the
// degeneracy test here and then collapse to collinear once the file is written, punching the very
// hole this pass prevents. Snapping makes the check see what will be written.
QuantizedPointMap vmap(Q, std::min(n_tri * 3, size_t(1) << 22));
std::vector<Vec3d> vert;
std::vector<int> vid(n_tri * 3);
for (size_t i = 0; i < n_tri * 3; ++i) {
const Vec3f &p = geometry.pos[i];
const int id = vmap.get_or_set(p, int(vert.size()));
if (vmap.inserted())
vert.emplace_back(double(grid_round(double(p.x()) * Q)) / Q,
double(grid_round(double(p.y()) * Q)) / Q,
double(grid_round(double(p.z()) * Q)) / Q);
vid[i] = id;
}
// Dropped: faces whose corners welded together, and needles - distinct but collinear on this
// grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly
// the on-edge-vertex topology the pass below closes.
std::vector<std::array<int, 3>> faces;
faces.reserve(n_tri);
for (size_t t = 0; t < n_tri; ++t) {
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
if (a == b || b == c || a == c)
continue;
const Vec3d u = vert[size_t(b)] - vert[size_t(a)];
const Vec3d w = vert[size_t(c)] - vert[size_t(a)];
if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ)
continue;
faces.push_back({ a, b, c });
}
for (int iter = 0; iter < opts.max_iters; ++iter) {
std::unordered_map<uint64_t, int> e_count;
for (const auto &f : faces)
for (int e = 0; e < 3; ++e)
++e_count[edge_key(f[size_t(e)], f[size_t((e + 1) % 3)])];
std::unordered_set<int> bverts;
for (const auto &[key, c] : e_count) {
if (c != 1)
continue;
bverts.insert(int(uint32_t(key >> 32)));
bverts.insert(int(uint32_t(key & 0xFFFFFFFFu)));
}
if (bverts.empty())
break;
const std::vector<int> bv(bverts.begin(), bverts.end());
struct Split { int a, b; std::vector<int> mids; };
std::unordered_map<size_t, Split> splits;
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto &f = faces[fi];
for (int e = 0; e < 3; ++e) {
const int a = f[size_t(e)], b = f[size_t((e + 1) % 3)];
if (e_count[edge_key(a, b)] != 1)
continue; // only a boundary edge carries an unresolved T-junction
const Vec3d A = vert[size_t(a)];
const Vec3d ev = vert[size_t(b)] - A;
const double elen2 = ev.squaredNorm();
if (elen2 < 1e-20)
continue;
std::vector<std::pair<double, int>> found;
for (const int c : bv) {
if (c == a || c == b)
continue;
const Vec3d cv = vert[size_t(c)] - A;
const double tp = cv.dot(ev) / elen2;
if (tp <= 1e-4 || tp >= 1.0 - 1e-4)
continue; // strictly between the ends
if ((cv - ev * tp).squaredNorm() < on_tol2)
found.emplace_back(tp, c);
}
if (!found.empty()) {
std::sort(found.begin(), found.end(),
[](const auto &x, const auto &y) { return x.first < y.first; });
Split sp{ a, b, {} };
for (const auto &m : found)
sp.mids.push_back(m.second);
splits.emplace(fi, std::move(sp));
break; // one site per face per pass; iteration handles cascades
}
}
}
if (splits.empty())
break;
std::vector<std::array<int, 3>> next;
next.reserve(faces.size() + splits.size() * 2);
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto it = splits.find(fi);
if (it == splits.end()) {
next.push_back(faces[fi]);
continue;
}
const auto &f = faces[fi];
const auto &sp = it->second;
const int apex = (f[0] != sp.a && f[0] != sp.b) ? f[0]
: (f[1] != sp.a && f[1] != sp.b) ? f[1]
: f[2];
// Walk the base the way the face already traverses it, so the winding survives.
bool dir_ab = false;
for (int e = 0; e < 3; ++e)
if (f[size_t(e)] == sp.a && f[size_t((e + 1) % 3)] == sp.b) {
dir_ab = true;
break;
}
std::vector<int> seq;
if (dir_ab) {
seq.push_back(sp.a);
seq.insert(seq.end(), sp.mids.begin(), sp.mids.end());
seq.push_back(sp.b);
} else {
seq.push_back(sp.b);
seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend());
seq.push_back(sp.a);
}
for (size_t s = 0; s + 1 < seq.size(); ++s)
next.push_back({ seq[s], seq[s + 1], apex });
}
faces.swap(next);
}
TriSoup out;
out.pos.reserve(faces.size() * 3);
out.nrm.reserve(faces.size() * 3);
for (const auto &f : faces) {
const Vec3f a = vert[size_t(f[0])].cast<float>();
const Vec3f b = vert[size_t(f[1])].cast<float>();
const Vec3f c = vert[size_t(f[2])].cast<float>();
Vec3f nrm = (b - a).cross(c - a);
const float len = nrm.norm();
nrm = (len > 0.f) ? Vec3f(nrm / len) : Vec3f(0.f, 0.f, 1.f);
out.pos.insert(out.pos.end(), { a, b, c });
out.nrm.insert(out.nrm.end(), { nrm, nrm, nrm });
}
return out;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,48 @@
#pragma once
// T-junction resolution and edge-defect accounting.
//
// Decimation can collapse a long edge whose interior still carries neighbouring triangles' vertices.
// Those then sit *on* an edge rather than at an end: watertight vertex-for-vertex, but the edge has
// one incident face on one side, which a slicer reads as an open boundary. This splits the offending
// face into a fan so every on-edge vertex becomes a real corner.
#include <cstdint>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
struct EdgeDefects
{
size_t open = 0, non_manifold = 0, triangles = 0;
};
// Welds at the export grid first: counting on the un-snapped mesh reports defects the file does not
// have and misses ones it does.
EdgeDefects count_edge_defects(const TriSoup &geometry, double quant = WELD_GRID_EXPORT);
// Triangles a slicer would drop as degenerate. Each one, removed, punches a hole - so a non-zero
// count means watertight only on paper.
size_t count_area_slivers(const TriSoup &geometry);
struct RepairOptions
{
// Coordinates are snapped onto this grid, matching the precision files are written with.
double weld_quant = WELD_GRID_EXPORT;
// How far off an edge a vertex may sit and still count as on it. Well above the harvest
// tolerance, since harvesting leaves a region flat only to within that, making a collapsed edge a
// chord the on-edge vertices deviate from by about as much. Still far below the weld grid.
double on_seg_tol = 0.02;
// Splitting one face can expose another behind it, so the pass cascades.
int max_iters = 16;
};
TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {});
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,414 @@
#include "TextureBakeSubdivide.hpp"
#include <algorithm>
#include <cmath>
#include <unordered_map>
namespace Slic3r {
namespace TextureBake {
namespace {
double edge_len_sq(const VertStore &v, int a, int b)
{
return (v.pos[size_t(a)] - v.pos[size_t(b)]).squaredNorm();
}
// Both indexers accumulate raw, area-weighted cross products and normalise once at the end.
void normalize_store_normals(VertStore &verts)
{
for (Vec3d &n : verts.nrm) {
const double len = n.norm();
n = (len > 0.0) ? Vec3d(n / len) : Vec3d(0.0, 0.0, 1.0);
}
}
// Keyed by the raw parent-vertex pair rather than by position: two sharp-edge copies of one point
// need their own midpoints, since their normals differ even though the position does not.
int get_midpoint(VertStore &verts, QuantizedPointMap &cache, int a, int b,
QuantizedPointMap *pos_canon_map)
{
const int lo = std::min(a, b), hi = std::max(a, b);
if (const int cached = cache.get_key(lo, hi, 0); cached != -1)
return cached;
const Vec3d m = (verts.pos[size_t(a)] + verts.pos[size_t(b)]) * 0.5;
Vec3d n = verts.nrm[size_t(a)] + verts.nrm[size_t(b)];
const double nl = n.norm();
n = (nl > 0.0) ? Vec3d(n / nl) : verts.nrm[size_t(a)];
const int idx = verts.push(m, n);
if (!verts.wgt.empty())
verts.wgt.push_back((verts.wgt[size_t(a)] + verts.wgt[size_t(b)]) * 0.5);
if (!verts.canon.empty() && pos_canon_map != nullptr)
verts.canon.push_back(pos_canon_map->get_or_set(float(m.x()), float(m.y()), float(m.z()), idx));
cache.get_or_set_key(lo, hi, 0, idx);
return idx;
}
struct PassResult
{
std::vector<int> indices;
std::vector<uint8_t> face_excluded;
std::vector<int> face_parent_id;
bool changed = false;
bool capped = false;
};
// Three steps, so that no T-junction can appear:
// 1. Mark every too-long edge globally, so both triangles on a shared edge decide alike.
// 1.5 Predict the exact resulting count from the marks (0->1, 1->2, 2->3, 3->4) and abort the
// *whole* pass if it exceeds the cap - a partial pass leaves split parents beside unsplit
// neighbours, the very crack step 1 prevents.
// 2. Rebuild, allocating once at the now-known size.
PassResult subdivide_pass(VertStore &verts, const std::vector<int> &indices, double max_edge_length,
int safety_cap, const std::vector<uint8_t> &face_excluded,
QuantizedPointMap *pos_canon_map, const std::vector<int> &face_parent_id)
{
PassResult out;
const double max_sq = max_edge_length * max_edge_length;
const size_t tri_count = indices.size() / 3;
const bool have_canon = !verts.canon.empty();
QuantizedPointMap mid_cache(1.0, 1 << 16);
QuantizedPointMap split_edges(1.0, 1 << 16);
// With canonical ids the key is the canonical *position* id, so split copies either side of a
// sharp edge see one another's decision; without them the vertex index serves.
const auto key_of = [&](int v) -> int64_t { return have_canon ? verts.canon[size_t(v)] : v; };
const auto mark_edge = [&](int a, int b) {
const int64_t u = key_of(a), v = key_of(b);
if (u < v) split_edges.get_or_set_key(u, v, 0, 1);
else split_edges.get_or_set_key(v, u, 0, 1);
};
const auto is_marked = [&](int a, int b) {
const int64_t u = key_of(a), v = key_of(b);
return (u < v ? split_edges.get_key(u, v, 0) : split_edges.get_key(v, u, 0)) != -1;
};
// Step 1. An excluded triangle marks none of its own edges, so its interior never refines; its
// boundary edges are still marked by an included neighbour, and it follows that split.
for (size_t t = 0; t < tri_count; ++t) {
if (!face_excluded.empty() && face_excluded[t])
continue;
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
if (edge_len_sq(verts, a, b) > max_sq) mark_edge(a, b);
if (edge_len_sq(verts, b, c) > max_sq) mark_edge(b, c);
if (edge_len_sq(verts, c, a) > max_sq) mark_edge(c, a);
}
if (split_edges.size() == 0) {
out.indices = indices;
out.face_excluded = face_excluded;
out.face_parent_id = face_parent_id;
return out; // changed stays false: nothing left to refine
}
// Step 1.5.
size_t predicted = 0;
for (size_t t = 0; t < tri_count; ++t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
const int n = int(is_marked(a, b)) + int(is_marked(b, c)) + int(is_marked(c, a));
predicted += (n == 0) ? 1 : size_t(n + 1);
}
if (predicted > size_t(safety_cap)) {
out.indices = indices;
out.face_excluded = face_excluded;
out.face_parent_id = face_parent_id;
out.capped = true;
return out; // coarser than asked for, but watertight
}
// Step 2.
out.indices.resize(predicted * 3);
if (!face_excluded.empty())
out.face_excluded.resize(predicted);
if (!face_parent_id.empty())
out.face_parent_id.resize(predicted);
size_t wi = 0, fi = 0;
const auto emit_face_data = [&](uint8_t excl, int pid, int times) {
for (int k = 0; k < times; ++k) {
if (!out.face_excluded.empty()) out.face_excluded[fi] = excl;
if (!out.face_parent_id.empty()) out.face_parent_id[fi] = pid;
++fi;
}
};
const auto emit = [&](int x, int y, int z) {
out.indices[wi++] = x; out.indices[wi++] = y; out.indices[wi++] = z;
};
for (size_t t = 0; t < tri_count; ++t) {
const int a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2];
const uint8_t excl = face_excluded.empty() ? uint8_t(0) : face_excluded[t];
const int pid = face_parent_id.empty() ? 0 : face_parent_id[t];
const bool s_ab = is_marked(a, b), s_bc = is_marked(b, c), s_ca = is_marked(c, a);
const int n = int(s_ab) + int(s_bc) + int(s_ca);
if (n == 0) {
emit(a, b, c);
emit_face_data(excl, pid, 1);
} else if (n == 3) {
// a
// / \
// mCA-mAB
// / \ / \
// c--mBC--b
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, m_ab, m_ca);
emit(m_ab, b, m_bc);
emit(m_ca, m_bc, c);
emit(m_ab, m_bc, m_ca);
emit_face_data(excl, pid, 4);
} else if (n == 1) {
if (s_ab) {
const int m = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
emit(a, m, c);
emit(m, b, c);
} else if (s_bc) {
const int m = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
emit(a, b, m);
emit(a, m, c);
} else {
const int m = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, b, m);
emit(m, b, c);
}
emit_face_data(excl, pid, 2);
} else {
// A corner triangle on the untouched-edge vertex, then the remaining quadrilateral split
// along the midpoint-to-midpoint diagonal, which keeps the winding consistent.
//
// A sliver parent propagates: that inner diagonal inherits half the short edge and hands
// the sliver to two children per pass. No better diagonal exists - one avoiding the
// midpoints must pass through one of them, giving a zero-area triangle. Regularization
// removes such slivers before the mesh reaches here.
if (!s_ab) { // fan from c
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, b, m_bc);
emit(a, m_bc, m_ca);
emit(c, m_ca, m_bc);
} else if (!s_bc) { // fan from a
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_ca = get_midpoint(verts, mid_cache, c, a, pos_canon_map);
emit(a, m_ab, m_ca);
emit(m_ab, b, c);
emit(m_ab, c, m_ca);
} else { // fan from b
const int m_ab = get_midpoint(verts, mid_cache, a, b, pos_canon_map);
const int m_bc = get_midpoint(verts, mid_cache, b, c, pos_canon_map);
emit(b, m_bc, m_ab);
emit(a, m_ab, m_bc);
emit(a, m_bc, c);
}
emit_face_data(excl, pid, 3);
}
}
out.changed = true;
return out;
}
} // namespace
IndexedMesh to_indexed_fast(const TriSoup &geometry)
{
// Preview path: a plain position merge - no clustering, no sharp-edge splitting, no canonical ids.
IndexedMesh out;
const size_t n = geometry.pos.size();
QuantizedPointMap vert_map(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
out.indices.resize(n);
const bool has_w = !geometry.exclude_weight.empty();
for (size_t i = 0; i < n; ++i) {
const Vec3f &p = geometry.pos[i];
const Vec3f nf = geometry.nrm.empty() ? Vec3f(0.f, 0.f, 1.f) : geometry.nrm[i];
const int idx = vert_map.get_or_set(p, int(out.verts.count()));
if (vert_map.inserted()) {
out.verts.push(p.cast<double>(), nf.cast<double>());
if (has_w)
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
} else {
out.verts.nrm[size_t(idx)] += nf.cast<double>();
// Merge exclusion by maximum: any excluded face marks the shared vertex.
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(idx)])
out.verts.wgt[size_t(idx)] = double(geometry.exclude_weight[i]);
}
out.indices[i] = idx;
}
normalize_store_normals(out.verts);
return out;
}
IndexedMesh to_indexed(const TriSoup &geometry)
{
// Export path. Two vertices at one position merge only when their face normals agree to within
// SUBDIVIDE_SHARP_ANGLE_DEG, which keeps a cylinder from faceting while stopping a cube's edge
// normal from leaking into the flat face interiors as subdivision carries it inward.
IndexedMesh out;
out.has_canon = true;
const size_t n = geometry.pos.size();
const bool has_w = !geometry.exclude_weight.empty();
const double sharp_cos = std::cos(SUBDIVIDE_SHARP_ANGLE_DEG * M_PI / 180.0);
// Per-face normals: unit for the angle test, raw for the area-weighted accumulation.
std::vector<Vec3d> face_unit(n), face_raw(n);
for (size_t t = 0; t + 2 < n; t += 3) {
const Vec3d a = geometry.pos[t].cast<double>();
const Vec3d b = geometry.pos[t + 1].cast<double>();
const Vec3d c = geometry.pos[t + 2].cast<double>();
const Vec3d r = (b - a).cross(c - a);
const double len = r.norm();
const Vec3d u = (len > 0.0) ? Vec3d(r / len) : Vec3d(0.0, 0.0, 1.0);
for (int v = 0; v < 3; ++v) {
face_unit[t + size_t(v)] = u;
face_raw[t + size_t(v)] = r;
}
}
out.indices.resize(n);
out.pos_canon_map = QuantizedPointMap(WELD_GRID_GEOMETRY, std::min(n, size_t(1) << 22));
struct Cluster { int idx; Vec3d fn_unit; };
std::unordered_map<int, std::vector<Cluster>> clusters_by_canon;
for (size_t i = 0; i < n; ++i) {
const Vec3f &p = geometry.pos[i];
// The first vertex at a position becomes its canonical id; later split copies share it.
const int canon_id = out.pos_canon_map.get_or_set(p, int(out.verts.count()));
const bool fresh_position = out.pos_canon_map.inserted();
const auto add_vertex = [&](int canon) {
const int idx = out.verts.push(p.cast<double>(), face_raw[i]);
if (has_w)
out.verts.wgt.push_back(double(geometry.exclude_weight[i]));
out.verts.canon.push_back(canon);
return idx;
};
if (fresh_position) {
const int idx = add_vertex(canon_id);
clusters_by_canon[canon_id].push_back({ idx, face_unit[i] });
out.indices[i] = idx;
continue;
}
std::vector<Cluster> &clusters = clusters_by_canon[canon_id];
bool matched = false;
for (Cluster &cl : clusters) {
if (cl.fn_unit.dot(face_unit[i]) < sharp_cos)
continue;
out.verts.nrm[size_t(cl.idx)] += face_raw[i];
if (has_w && double(geometry.exclude_weight[i]) > out.verts.wgt[size_t(cl.idx)])
out.verts.wgt[size_t(cl.idx)] = double(geometry.exclude_weight[i]);
// Track the running average, so gradual curvature stays in one cluster instead of
// fragmenting when a distant face exceeds the threshold against the seed's fixed normal.
cl.fn_unit += face_unit[i];
if (const double rl = cl.fn_unit.norm(); rl > 0.0)
cl.fn_unit /= rl;
out.indices[i] = cl.idx;
matched = true;
break;
}
if (!matched) {
// A sharp-edge split: a new vertex at the same position, sharing its canonical id.
const int idx = add_vertex(canon_id);
clusters.push_back({ idx, face_unit[i] });
out.indices[i] = idx;
}
}
normalize_store_normals(out.verts);
return out;
}
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
const std::vector<uint8_t> &face_excluded)
{
TriSoup out;
const size_t tri_count = indices.size() / 3;
out.pos.resize(tri_count * 3);
out.nrm.resize(tri_count * 3);
const bool want_weights = !face_excluded.empty() || !verts.wgt.empty();
if (want_weights)
out.exclude_weight.resize(tri_count * 3);
for (size_t t = 0; t < tri_count; ++t) {
// The per-face flag, not the interpolated weight: merging by maximum can push an *included*
// face's corners to 1 when it borders two excluded neighbours, wrongly excluding it.
const bool have_face_flag = !face_excluded.empty();
const float face_w = have_face_flag ? (face_excluded[t] ? 1.f : 0.f) : 0.f;
for (int v = 0; v < 3; ++v) {
const size_t vidx = size_t(indices[t * 3 + size_t(v)]);
out.pos[t * 3 + size_t(v)] = verts.pos[vidx].cast<float>();
out.nrm[t * 3 + size_t(v)] = verts.nrm[vidx].cast<float>();
if (want_weights)
out.exclude_weight[t * 3 + size_t(v)] =
have_face_flag ? face_w : float(verts.wgt[vidx]);
}
}
return out;
}
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
const std::vector<uint8_t> &face_excluded, bool fast, int safety_cap,
const SubdivideProgressFn &on_progress)
{
SubdivideResult result;
if (geometry.empty() || max_edge_length <= 0.0) {
result.geometry = geometry;
return result;
}
IndexedMesh indexed = fast ? to_indexed_fast(geometry) : to_indexed(geometry);
QuantizedPointMap *canon_map = indexed.has_canon ? &indexed.pos_canon_map : nullptr;
std::vector<int> current_indices = indexed.indices;
std::vector<uint8_t> current_excluded = face_excluded;
const size_t initial_tris = indexed.indices.size() / 3;
std::vector<int> current_parent(initial_tris);
for (size_t i = 0; i < initial_tris; ++i)
current_parent[i] = int(i);
for (int iter = 0; iter < SUBDIVIDE_MAX_ITERATIONS; ++iter) {
if (current_indices.size() / 3 >= size_t(safety_cap)) {
result.safety_cap_hit = true;
break;
}
PassResult pass = subdivide_pass(indexed.verts, current_indices, max_edge_length, safety_cap,
current_excluded, canon_map, current_parent);
current_indices = std::move(pass.indices);
if (!pass.face_excluded.empty())
current_excluded = std::move(pass.face_excluded);
if (!pass.face_parent_id.empty())
current_parent = std::move(pass.face_parent_id);
if (pass.capped || current_indices.size() / 3 >= size_t(safety_cap))
result.safety_cap_hit = true;
if (on_progress) {
// Reported after the pass, so the value falls each iteration instead of lagging a step.
double max_edge_sq = 0.0;
for (size_t t = 0; t + 2 < current_indices.size(); t += 3) {
const int a = current_indices[t], b = current_indices[t + 1], c = current_indices[t + 2];
max_edge_sq = std::max({ max_edge_sq, edge_len_sq(indexed.verts, a, b),
edge_len_sq(indexed.verts, b, c),
edge_len_sq(indexed.verts, c, a) });
}
if (!on_progress(std::min(0.95, double(iter + 1) / SUBDIVIDE_MAX_ITERATIONS),
current_indices.size() / 3, std::sqrt(max_edge_sq)))
break; // whole passes only, so what we have is still crack-free
}
if (!pass.changed || result.safety_cap_hit)
break;
}
result.geometry = to_non_indexed(indexed.verts, current_indices, current_excluded);
result.face_parent_id = std::move(current_parent);
return result;
}
} // namespace TextureBake
} // namespace Slic3r

View File

@@ -0,0 +1,83 @@
#pragma once
// Adaptive subdivision to a target edge length, by global marked-edge (red-green) refinement rather
// than longest-edge bisection. Marking is global, so two triangles sharing an edge always agree and
// the result is crack-free by construction. A triangle is rebuilt from its marked-edge count: 0
// keeps, 1 bisects, 2 fans into three, 3 does the regular 1->4 split.
//
// The 1->4 case is what keeps the tessellation regular - its children are similar to the parent. An
// irregular one shows up after displacement as adjacent triangles tilting alternately, i.e. noise.
#include <functional>
#include <vector>
#include "TextureBakeIndex.hpp"
namespace Slic3r {
namespace TextureBake {
// Memory guard for the stages downstream. At roughly 145 bytes per triangle this is about 2.9 GB.
static constexpr int SUBDIVIDE_SAFETY_CAP = 16'000'000;
// Vertices at one position stay separate when their faces disagree by more than this: a cube keeps
// hard edges, a cylinder keeps averaged ones.
static constexpr double SUBDIVIDE_SHARP_ANGLE_DEG = 30.0;
// A depth bound, not a work bound: the loop stops as soon as a pass changes nothing.
static constexpr int SUBDIVIDE_MAX_ITERATIONS = 12;
// Built by the indexers, appended to by the passes. Double precision so repeated midpointing does
// not drift.
struct VertStore
{
std::vector<Vec3d> pos;
std::vector<Vec3d> nrm;
std::vector<double> wgt; // exclusion weights; empty when the caller supplied none
std::vector<int> canon; // canonical position ids; empty in fast mode
size_t count() const { return pos.size(); }
int push(const Vec3d &p, const Vec3d &n)
{
const int idx = int(pos.size());
pos.push_back(p);
nrm.push_back(n);
return idx;
}
};
struct IndexedMesh
{
VertStore verts;
std::vector<int> indices; // 3 per triangle
QuantizedPointMap pos_canon_map{ WELD_GRID_GEOMETRY, 256 };
bool has_canon = false;
};
// Fraction, triangle count, longest remaining edge. Returning false cancels; what comes back is
// still watertight, because passes apply whole or not at all.
using SubdivideProgressFn = std::function<bool(double fraction, size_t triangles, double longest_edge)>;
struct SubdivideResult
{
TriSoup geometry;
// Output triangle -> input triangle it descends from, so per-face data survives with no remap.
std::vector<int> face_parent_id;
bool safety_cap_hit = false;
};
// `face_excluded`: one entry per input triangle; non-zero means its interior is never refined. Its
// edges still split when an included neighbour marks them, so no T-junction appears at the boundary.
// `fast` selects the cheap position-only indexer for previews.
SubdivideResult subdivide(const TriSoup &geometry, double max_edge_length,
const std::vector<uint8_t> &face_excluded = {}, bool fast = false,
int safety_cap = SUBDIVIDE_SAFETY_CAP,
const SubdivideProgressFn &on_progress = {});
// Displacement needs the same welding and sharp-edge clustering.
IndexedMesh to_indexed(const TriSoup &geometry);
IndexedMesh to_indexed_fast(const TriSoup &geometry);
TriSoup to_non_indexed(const VertStore &verts, const std::vector<int> &indices,
const std::vector<uint8_t> &face_excluded);
} // namespace TextureBake
} // namespace Slic3r

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,934 @@
#ifndef slic3r_TextureDisplacement_hpp_
#define slic3r_TextureDisplacement_hpp_
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include <cereal/cereal.hpp>
#include <cereal/types/array.hpp> // view_project_matrix is a std::array<float, 12>
#include <cereal/types/string.hpp>
#include <cereal/types/vector.hpp>
#include <array>
#include "Point.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
namespace Slic3r {
class ModelVolume;
// Bits of subdivide_mesh_adaptive()'s per-triangle `refine_region` mask. See that function.
static constexpr uint8_t REFINE_PAINTED = 1;
static constexpr uint8_t REFINE_BORDER = 2;
// Progress/cancellation hook for the (potentially multi-second) bake. Called with a 0..100
// percentage; return false to abort. See build_texture_displacement().
using DisplacementProgressFn = std::function<bool(int)>;
// Maximum number of simultaneous texture-displacement layers a single ModelVolume can hold.
// Each layer owns its own paint mask (ModelVolume::texture_displacement_facet(slot)), so this
// is also the number of independent EnforcerBlockerType selectors kept per volume.
static constexpr size_t TEXTURE_DISPLACEMENT_MAX_LAYERS = 8;
// How a layer's height texture is sampled outside its [0, 1) tile when tiling is enabled. Ignored
// (always clamp) when TextureDisplacementLayer::tile_enabled is false.
enum class TextureTileMethod : int
{
Repeat = 0, // wrap around, tile i and tile i+1 are identical (default)
MirroredRepeat = 1, // wrap around, every other tile is mirrored (no visible seam at tile edges)
};
// How a layer's texture is mapped onto the mesh.
enum class TextureProjectionMethod : int
{
// Standard *blended* tri-planar projection: the texture is sampled once per world axis (the
// XY, XZ and YZ planes) and the three samples are blended per vertex, weighted by that
// vertex's own normal raised to TRIPLANAR_BLEND_SHARPNESS.
//
// Earlier versions instead *hard-picked* the single axis most aligned with the normal. That
// has a real, visible failure mode at any edge where the dominant axis flips: on a +X face the
// planar coordinate is (y, z), on a -Y face it is (x, z), so at the shared edge u jumps from
// y_edge to x_edge. On a box centred near the origin those two agree at the (+,+) and (-,-)
// corners (making them look fine) but differ by the full corner width at the (+,-) and (-,+)
// corners, which is exactly the "two bad corners, two good ones" seam that was reported.
// Blending across the transition removes that hard discontinuity by construction.
Triplanar = 0,
// Wrapped around an axis running through the patch's centroid. The axis itself is picked
// automatically as the world axis *least* aligned with the patch's average normal (since a
// cylinder's own axis is perpendicular to its outward radial normal) - a reasonable default
// for roughly cylindrical selections, not a precise fit for arbitrary geometry.
Cylindrical = 1,
// Wrapped around the patch's centroid using longitude/latitude - reasonable for roughly
// spherical/rounded selections, again an approximation rather than an exact geodesic map.
Spherical = 2,
// Real UV unwrap of the painted patch - a proper low-distortion flattening rather than a
// planar/cylindrical/spherical approximation. The patch is first cut into charts along its
// sharp edges and each chart is flattened on its own (see compute_patch_unwrap()), so a patch
// that is not a single developable surface still unwraps sensibly. Falls back to Triplanar for
// any chart that cannot be flattened at all.
LSCM = 3,
// Flat projection along a fixed direction captured from the 3D camera ("project from view"): the
// texture is laid onto the painted area as seen from that angle, like a decal projector. Single
// planar map (no per-face axis switch), so it can smear on faces turned away from the projector -
// that is inherent to view projection and is the user's call, not a bug. The projector's two
// in-plane axes live in TextureDisplacementLayer::view_project_right/up.
ViewProjected = 4,
};
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
// - i.e. the unwrap is cut there rather than being forced to flatten across it.
//
// The whole point of this being a threshold rather than "flatten everything as one piece": three
// faces meeting at a cube corner are not developable, so a single-chart solve has to distort them
// badly to lie flat (they splay out into a fan, which is what "it merges all the edges into a
// triangle" describes). Cutting at the 90-degree edges instead lets each face flatten exactly.
// Meanwhile a smoothly curved surface - a subdivided sphere, say - has only small angles between
// neighbouring triangles, stays a single chart, and unwraps as one piece the way it should.
static constexpr float LSCM_DEFAULT_SEAM_ANGLE_DEG = 30.f;
// Exponent the tri-planar blend weights are raised to (see TextureProjectionMethod::Triplanar).
// Higher means a tighter, more "hard-edged" transition between the three axis projections; lower
// means a wider cross-fade. 4 is the usual default: tight enough that a flat face is sampled
// almost purely along its own axis, wide enough that a 90-degree edge has no visible hard seam.
static constexpr float TRIPLANAR_BLEND_SHARPNESS = 4.f;
// How a layer's displacement combines with the displacement accumulated by the layers below it
// (i.e. those in lower slots), evaluated per vertex. Analogous to an image editor's layer blend
// modes, except the quantity being blended is a signed displacement distance in mm rather than a
// pixel value.
//
// Add/Subtract are in mm and need no further explanation. Multiply/Divide are *scaling* operations
// and therefore need a unit convention: they treat the layer's own value as a unitless factor
// relative to 1 mm. That makes `depth_mm` act as a gain - a layer with depth 1 mm and a white
// (1.0) texel multiplies the accumulated relief by exactly 1, i.e. leaves it unchanged - which is
// the behaviour that makes a Multiply layer usable as a mask over the layers beneath it.
enum class TextureBlendMode : int
{
Add = 0, // acc + value (default; several layers pile their relief up together)
Subtract = 1, // acc - value (carve this layer's relief out of the layers below)
Multiply = 2, // acc * (value / 1mm) (mask/modulate the layers below by this layer)
Divide = 3, // acc / (value / 1mm) (inverse mask; guarded against a zero/near-zero divisor)
};
// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`.
// Shared by the bake/preview path and exposed for tests.
float blend_displacement(float accumulated, float value, TextureBlendMode mode);
// Where one unwrap island (chart) sits in UV space, on top of wherever compute_patch_unwrap() first
// packed it. This is what the UV editor's drag/rotate gestures write to, so a user can lay the
// islands out by hand - move them, rotate them, overlap them - rather than being stuck with the
// automatic packing.
//
// Indexed by chart id, which compute_patch_unwrap() assigns in first-encountered-triangle order. That
// is stable for a given patch and seam angle, but *not* across a change to either: repainting the
// patch, or moving the seam-angle slider, can renumber the charts and so leave a hand-placed island
// applied to a different one. Accepted deliberately - the alternative is a persistent chart identity
// that survives arbitrary re-segmentation, which is a much larger problem than this feature warrants.
struct TextureIsland
{
Vec2f offset = Vec2f::Zero(); // in the unwrap's own mm space
float rotation_deg = 0.f; // about the island's own centroid
// About the island's own centroid too. 1 = the size compute_patch_unwrap() gave it, which is
// already its true surface area in mm - so scaling an island away from 1 deliberately makes its
// texel density differ from its neighbours'. See average_island_scales().
float scale = 1.f;
template<class Archive> void serialize(Archive &ar) { ar(offset, rotation_deg, scale); }
};
// Sets every island's scale to the mean of the current ones (Blender's "Average Islands Scale").
// Only meaningful after islands have been scaled by hand: compute_patch_unwrap() already sizes every
// chart to its true mm area, so a freshly unwrapped patch has uniform texel density to begin with.
void average_island_scales(std::vector<TextureIsland> &islands);
// One texture asset plus its projection/displacement parameters. Several layers may be painted
// onto overlapping areas of the same volume: their displacements are combined per vertex, in slot
// order, each layer folding into the total via its own TextureBlendMode (see
// build_texture_displacement()). This is what "layered/blended" texture displacement means here.
struct TextureDisplacementLayer
{
// Index into ModelVolume::texture_displacement_facets, assigned once when the layer is
// created. Not reused for the lifetime of the ModelVolume, so a deleted layer's slot simply
// becomes unused rather than being handed to a different layer.
int slot = -1;
std::string name;
// Path on the local filesystem the image was loaded from (informational; may be stale or
// empty, e.g. after loading a .3mf on a different machine).
std::string path;
// Path inside the .3mf archive once saved (empty until the project is saved once).
std::string path_in_3mf;
// Raw encoded image bytes. Only 8-bit grayscale PNG is understood by decode_height_texture()
// (libslic3r has no GUI image toolkit available); the GUI converts any imported image to that
// format before storing it here, so the baking code never needs to depend on wxWidgets.
std::shared_ptr<std::vector<unsigned char>> image_data;
float depth_mm = 0.4f; // maximum displacement along the surface normal, in mm
float tiling_scale = 12.5f; // size of one texture tile, in mm
float rotation_deg = 0.f;
Vec2f offset = Vec2f::Zero();
bool invert = false;
// The height value that means "don't move this vertex". The sampled height (0..1) has this
// subtracted before being scaled by depth_mm, so with the default of 0 the surface only ever
// moves *outwards* (the classic height-map convention), while 0.5 makes mid-grey neutral and
// lets darker texels cut *into* the surface - an engraved-and-embossed result from one map.
//
// Cutting inward is not free: vertices move along their own normals, which converge inside a
// concave corner and inside a thin wall, so a large depth_mm against a small feature really can
// fold the surface through itself. There is no cheap way to detect that here (it needs a full
// self-intersection test on the displaced mesh), so the GUI warns rather than promising safety.
float midlevel = 0.f;
// Optional blur applied to the decoded height map before it is sampled, in [0, 1]: 0 is the raw
// texture, 1 the strongest blur. Softens the relief (rounds hard edges, removes speckle) without
// needing a pre-blurred source image. Applied in decode_height_texture(), so it feeds the true
// preview, the UV editor backdrop and the bake identically.
float smoothing = 0.f;
// Optional feathering of the displacement toward the edge of the painted patch. When enabled, the
// displacement is scaled down as a vertex approaches the patch boundary, so the relief blends
// smoothly into the surrounding surface instead of ending abruptly. `edge_smoothing_amount` in
// (0, 1] sets how far the fade reaches into the patch: small values only soften a thin band at the
// very edge, 1 fades the whole patch to nothing (the painted face comes out flat). Off by default.
bool edge_smoothing = false;
float edge_smoothing_amount = 0.5f;
// Only used by TextureProjectionMethod::LSCM: when set, a fresh unwrap is laid out as a connected
// net (adjacent charts unfolded edge-to-edge along a spanning tree, see compute_connected_net())
// rather than as separately packed islands. On by default. Hand-moving an island overrides its
// placement until the next re-unwrap.
bool auto_connect_islands = true;
// When false, the texture is sampled once (clamped to its edge pixels outside [0, 1)) instead
// of being repeated - useful for a single decal-like placement rather than a repeating tile.
bool tile_enabled = true;
TextureTileMethod tile_method = TextureTileMethod::Repeat;
TextureProjectionMethod projection_method = TextureProjectionMethod::Triplanar;
// Only used by TextureProjectionMethod::LSCM. See LSCM_DEFAULT_SEAM_ANGLE_DEG.
float lscm_seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG;
// Only used by TextureProjectionMethod::LSCM: gap left between islands by the automatic packing,
// in the unwrap's mm space. Negative means "auto" (a small fraction of the packed size), which is
// what a patch that has never had the slider touched gets.
float island_padding_mm = -1.f;
// Only used by TextureProjectionMethod::LSCM: edges the unwrap is forced to cut along, on top of
// whatever the seam angle already cuts. Each pair is an undirected edge in *mesh vertex index*
// space (first < second). This is what "mark seam" (manual) and "cut island" (auto) both write to.
// Mesh-index space, so like the paint masks these are dropped on any topology change.
std::vector<std::pair<int, int>> lscm_seam_edges;
// Only used by TextureProjectionMethod::ViewProjected: the projector's in-plane axes, in the
// volume's *local* space, captured from the camera when the user hits "Project from view". A point
// projects to Vec2f(dot(pos, right), dot(pos, up)) before the usual tiling/rotation/offset.
Vec3f view_project_right = Vec3f::UnitX();
Vec3f view_project_up = Vec3f::UnitY();
// Also ViewProjected, and takes precedence over the two axes above when set: an exact *projective*
// map from a local-space position straight to a texture uv, written by the projection-frame overlay
// (the semi-transparent window dragged over the 3D view - its border becomes the uv unit square).
//
// Row-major 3x4, applied to the homogeneous point p~ = (x, y, z, 1):
// uv = ( row0.p~ / row2.p~ , row1.p~ / row2.p~ )
// The perspective divide is the whole point. view_project_right/up can only express an *affine*
// projection, which matches an orthographic camera exactly but not a perspective one - under
// perspective the near end of a part projects larger than the far end, and no pair of axes
// reproduces that. Folding the camera's full projection*view*model product into one matrix does.
// Because a point behind the projector has row2.p~ <= 0 and no meaningful uv, sampling must check
// the sign rather than divide blindly; see project_uv_projective().
//
// Note this map already includes placement, so the usual tiling/rotation/offset transform is NOT
// applied on top of it - the window's own position and size are the placement.
bool view_project_projective = false;
std::array<float, 12> view_project_matrix{};
// Only used by TextureProjectionMethod::LSCM: hand placement of the unwrap's islands, indexed by
// chart id (see TextureIsland). Shorter than the chart count simply means the missing ones are
// still where the automatic packing put them.
std::vector<TextureIsland> islands;
// Only used by TextureProjectionMethod::LSCM: persistent "join" groups, indexed by chart id. Charts
// that share a group id move together as one in the UV editor - this is what the explicit "Join"
// command records (over and above placing the child next to its parent). An entry of -1, or an index
// past the end of the vector, means the chart is its own singleton group (moves alone). Empty means
// every chart is a singleton. Same chart-renumbering caveat as `islands`: a re-unwrap can reshuffle
// chart ids, so this is meaningful only against the unwrap it was made on.
std::vector<int> island_groups;
// Only used by TextureProjectionMethod::LSCM: manual per-vertex UV edits made in the UV editor's
// Vertex/Edge select modes. Each pair is (mesh vertex index, its overriding raw-unwrap coordinate in
// mm) - the *raw* unwrap position, i.e. before the island transform, so the edited vertex still
// moves and rotates with its island. In compute_lscm_uvs() this replaces the automatic unwrap
// coordinate for that vertex; in the editor it edits the displayed geometry directly. Keyed in mesh-
// vertex space like lscm_seam_edges (dropped on a topology change). The raw coordinate is only
// meaningful against the current unwrap, so a re-unwrap clears these. A mesh vertex shared by several
// charts (a seam vertex) settles on one, matching compute_lscm_uvs()'s single-UV-per-vertex rule.
std::vector<std::pair<int, Vec2f>> lscm_uv_overrides;
// How this layer folds into the displacement accumulated by the layers below it. Ignored for
// the lowest-slot painted layer, which has nothing beneath it to combine with (the GUI shows
// it as the "Base" layer and hides the control).
TextureBlendMode blend_mode = TextureBlendMode::Add;
// Colour this layer's painted area from the texture's own colours, on top of displacing by its
// height. Only meaningful when the texture actually has colour (DecodedHeightTexture::has_color()):
// the shipped library is grayscale, so this does nothing there.
//
// Colour lands in the volume's mmu_segmentation_facets - the same per-triangle filament assignment
// the MMU paint gizmo writes - so its resolution is the *mesh's*, not the image's, and a triangle
// gets exactly one filament. That is why the adaptive subdivision has a colour criterion of its
// own (see subdivide_mesh_adaptive()): without triangles along a colour boundary there is nothing
// for the boundary to be drawn on.
bool color_enabled = false;
bool empty() const { return !image_data || image_data->empty(); }
template<class Archive> void save(Archive &ar) const
{
std::string blob = image_data ? std::string(image_data->begin(), image_data->end()) : std::string();
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
static_cast<int>(tile_method), static_cast<int>(projection_method), lscm_seam_angle_deg, islands,
static_cast<int>(blend_mode), midlevel, island_padding_mm, lscm_seam_edges, view_project_right,
view_project_up, smoothing, edge_smoothing, edge_smoothing_amount, auto_connect_islands, island_groups,
lscm_uv_overrides, view_project_projective, view_project_matrix, color_enabled);
}
template<class Archive> void load(Archive &ar)
{
std::string blob;
int tile_method_int = 0;
int projection_method_int = 0;
int blend_mode_int = 0;
ar(slot, name, path, path_in_3mf, blob, depth_mm, tiling_scale, rotation_deg, offset, invert, tile_enabled,
tile_method_int, projection_method_int, lscm_seam_angle_deg, islands, blend_mode_int, midlevel,
island_padding_mm, lscm_seam_edges, view_project_right, view_project_up, smoothing, edge_smoothing,
edge_smoothing_amount, auto_connect_islands, island_groups, lscm_uv_overrides, view_project_projective,
view_project_matrix, color_enabled);
image_data = blob.empty() ? nullptr : std::make_shared<std::vector<unsigned char>>(blob.begin(), blob.end());
tile_method = static_cast<TextureTileMethod>(tile_method_int);
projection_method = static_cast<TextureProjectionMethod>(projection_method_int);
blend_mode = static_cast<TextureBlendMode>(blend_mode_int);
}
};
// How a *mixed* palette entry - one that names two filaments rather than one - is turned into real
// per-facet paint. An MMU extrudes one filament at a time, so an intermediate colour exists only by
// interleaving two of them finely enough that the eye does the blending.
enum class ColorMixMode : int
{
// Horizontal bands: which of the two filaments a point takes depends on its height, so
// consecutive print layers alternate. This is how filament-blend prints actually work, and on a
// vertical-ish surface it reads as a genuinely smooth colour. On a near-horizontal surface a whole
// layer is one band, so the blend disappears - that is what XYDither is for.
ZBands = 0,
// An ordered (Bayer) checkerboard across the surface, at any orientation. Independent of layer
// height, but its cell is around the size of one facet, so a fine mix can read as texture rather
// than as a clean blend.
XYDither = 1,
};
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
// to texture_displacement_layers and consumed by build_texture_displacement().
struct TextureDisplacementOptions
{
// Whether the painted patch's *border* vertices - the ones also used by unpainted triangles -
// are displaced along with the rest, or pinned flat.
//
// Pinning them was originally justified as keeping the patch from tearing away from the
// surrounding surface. That reasoning no longer applies: since the bake became
// topology-preserving it only ever *moves* the input's own vertices, so a border vertex is one
// vertex shared by both regions and moving it simply tilts the unpainted triangles that use it -
// nothing can come apart. What pinning actually does is clamp the outermost ring of the relief to
// zero, which on a fully painted face collapses the pattern into a ring of steep ramps right at
// the edge (the "it doesn't extrude at the border" artifact). Displacing it is the default;
// pinning is kept for the case where the relief must not spill past the paint at all.
bool displace_border = true;
// Optional Laplacian relaxation of the displaced surface, run after all layers have been folded
// in - a post-process, not a texture filter (TextureDisplacementLayer::smoothing blurs the height
// map instead, before it is ever sampled). Rounds off the hard steps a bitmap height map leaves
// behind. Restricted to vertices the displacement actually moved, so the rest of the model keeps
// its exact geometry. `smooth_strength` in [0, 1] is how far each pass moves a vertex toward the
// average of its neighbours.
bool smooth_enabled = false;
float smooth_strength = 0.3f;
int smooth_iterations = 2;
// Hold the painted patch's outermost ring of vertices out of the smoothing. Those vertices sit
// next to unpainted ones that are pinned by definition, so relaxing them drags the rim of the
// relief back down toward the undisplaced surface - the pattern looks half-melted exactly where it
// meets the edge, however crisp the rest of it is. Excluding them keeps the border extruded at
// full depth and smooths only the interior. On by default; turn it off to soften the outer edge
// deliberately (which is a blunter version of the per-layer edge-smoothing falloff).
bool smooth_skip_border = true;
// Alternative bake pipeline, for side-by-side comparison. The path above is topology-preserving
// and needs the mesh prepared first; this one refines, removes slivers, displaces and optionally
// simplifies in one run. Off by default, and it produces no colour - it rebuilds the topology, so
// the per-facet assignment has nothing stable to attach to.
bool pipeline_v2 = false;
float v2_refine_mm = 0.3f;
bool v2_regularize = false;
int v2_max_triangles_k = 750; // 0 skips simplification, which is worth comparing on its own
// Colour, all of which belongs to the stack rather than to any one layer: it is about how the
// printer will realise the colours, not about which image they came from.
// Interleave pairs of filaments to get colours between them - so four loaded filaments offer far
// more than four colours. Off means every triangle takes one of the loaded filaments exactly.
bool color_mix_enabled = true;
ColorMixMode color_mix_mode = ColorMixMode::ZBands;
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
int color_despeckle = 2;
template<class Archive> void serialize(Archive &ar)
{
int mix_mode = int(color_mix_mode);
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, color_mix_enabled, mix_mode,
color_despeckle);
color_mix_mode = ColorMixMode(mix_mode);
}
};
// Decoded height (and, for a colour source image, colour) samples, independent of any GUI/OpenGL
// texture object so they can be evaluated from a background bake Job as well as from GUI-side
// preview code.
struct DecodedHeightTexture
{
std::vector<uint8_t> pixels; // height: row-major, top-to-bottom, one byte per pixel
// Colour: the same grid, three bytes per pixel, or empty when the source image was grayscale.
// A grayscale height map has no colour to give - `pixels` is not a colour, it is a height - so
// has_color() is what the whole colour feature keys off: a layer set to colour a model with a
// grayscale texture on it simply colours nothing.
std::vector<uint8_t> rgb;
int width = 0;
int height = 0;
bool empty() const { return width <= 0 || height <= 0 || pixels.empty(); }
bool has_color() const { return !empty() && rgb.size() == size_t(width) * size_t(height) * 3; }
// Bilinearly sampled height in [0, 1] at a normalized uv coordinate. When tile_enabled is false,
// a uv outside [0, 1) samples as 0 - the texture simply is not there, rather than its border
// row/column being smeared outward forever (which is what clamping the coordinate would do, and
// was a real reported bug). Callers rely on this to get a hard edge: it is how the projection
// frame's border becomes the edge of the displacement.
float sample(const Vec2f &uv, bool tile_enabled = true, TextureTileMethod tile_method = TextureTileMethod::Repeat) const;
// The same sample, in colour: linear RGB components in [0, 1]. Outside a non-tiled placement, and
// for a grayscale source, this is (0, 0, 0) - callers pair it with has_color() and with the
// height's own coverage rather than trying to read "no texture here" out of the colour itself.
Vec3f sample_color(const Vec2f &uv, bool tile_enabled = true,
TextureTileMethod tile_method = TextureTileMethod::Repeat) const;
// Where a uv lands on the texel grid: the four texels of the bilinear tap and their weights.
// Shared by sample() and sample_color(), so a layer's height and its colour can never end up
// read from different places in the image. False means the uv is outside a non-tiled placement -
// no texture there at all (see sample()).
struct TexelTap
{
int x0 = 0, y0 = 0, x1 = 0, y1 = 0;
float tx = 0.f, ty = 0.f;
};
bool texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method, TexelTap &out) const;
};
// Decode a layer's raw image bytes into sampleable height data, plus colour when the source has any.
// Both 8-bit grayscale PNGs (the shipped library, and anything imported before colour was kept) and
// colour PNGs are accepted; for a colour source the height is its luminance, using the same
// coefficients wxImage::ConvertToGreyscale() uses, so a texture imported as colour displaces exactly
// as it did when the importer flattened it to grey on the way in. Returns an empty
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
//
// Deliberately a callback rather than a function here: matching a colour to a filament is a
// *perceptual* question (CIEDE2000 over CIELAB), and that machinery - slic3r/Utils/ColorSpaceConvert
// and GuiColor - lives on the GUI side along with the list of filaments actually loaded. libslic3r
// samples the image and decides *where* colour changes; the GUI decides *which* filament each colour
// is. See GLGizmoTextureDisplacement::make_palette_quantizer().
using ColorQuantizeFn = std::function<int(const Vec3f &)>;
// Resolves a palette index plus a surface position to the filament index that position should print
// in. A pure entry ignores the position; a mixed one interleaves its two filaments per ColorMixMode.
//
// Deliberately separate from ColorQuantizeFn, and deliberately *not* used by the subdivision's colour
// criterion: that criterion asks where the **perceived** colour changes, and must not see the
// interleaving. Refining on every band or dither-cell boundary would spend the whole triangle budget
// drawing a pattern the eye is supposed to blend away.
using ColorResolveFn = std::function<int(int palette_index, const Vec3f &pos)>;
// One printable colour: either a loaded filament on its own, or a blend of two of them realised by
// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job.
struct PrintableColor
{
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
int a = 0; // filament index
int b = 0; // the second filament; == a for a pure entry
int num = 1; // a's share of the interleave, out of `den`
int den = 1;
bool is_mix() const { return a != b; }
};
// Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty
// palette means nothing is colouring, which is the state every one of these paths starts in.
struct TextureColorSettings
{
std::vector<PrintableColor> palette;
ColorMixMode mix_mode = ColorMixMode::ZBands;
float layer_height = 0.2f; // sizes the Z bands
float dither_cell_mm = 0.4f; // sizes the XY dither cells
int despeckle_passes = 2;
bool empty() const { return palette.empty(); }
};
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
// layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather
// than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas
// "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space
// tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic.
Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
// (in mm, dominant-axis planar, cylindrical, spherical, or CGAL LSCM output - any of them, all
// share this same final step). Exposed separately so build_texture_displacement() can route CGAL
// LSCM's per-patch UV solve through the same scale/rotate/offset controls as every other
// projection method, without going through project_texture_displacement_uv()'s own dispatch
// (which only knows how to compute the *analytic* methods from a single vertex + normal).
// `aspect` is the height map's width / height. It scales the v axis so a non-square image is not
// squeezed into a square tile: `tiling_scale` is the tile's size along u, and the tile is
// `tiling_scale * height / width` mm along v, which keeps texels square. 1 (the default) is the
// square case and leaves the coordinate exactly as it always was.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f);
// Applies a row-major 3x4 projective matrix (see TextureDisplacementLayer::view_project_matrix) to a
// local-space point, writing the resulting texture uv. Returns false - and leaves `uv` untouched -
// when the point lies behind the projector or on its plane (w <= 0), where there is no meaningful uv
// and dividing would produce a mirrored or infinite coordinate. Callers treat that as "no height".
bool project_uv_projective(const std::array<float, 12> &m, const Vec3f &position, Vec2f &uv);
// Sample a layer's height texture at a mesh-local position, honouring the layer's projection
// method, tiling scale, rotation, offset and tiling mode. Returns a height in [0, 1].
//
// This returns a *height* rather than a UV because TextureProjectionMethod::Triplanar is a blend
// of three separate axis projections and therefore takes three texture samples per vertex - there
// is no single UV that represents it. The other methods do map to one UV internally.
// - `normal` is this specific vertex's own normal; used only by Triplanar (for its blend weights).
// - `patch_center`/`patch_axis` describe the painted patch as a whole (its centroid, and - for
// Cylindrical only - the wrap axis); used only by the Cylindrical/Spherical methods.
// - `lscm_uv`, when non-null, is this vertex's precomputed LSCM coordinate and takes precedence
// over `layer.projection_method` (LSCM is a single per-patch solve, not a per-vertex formula,
// so build_texture_displacement() computes it once up front and passes it in here).
// patch_center/patch_axis are cheap to compute once per patch and passed through unchanged for
// every vertex rather than being re-derived per call.
float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
const Vec3f &position, const Vec3f &normal,
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr);
// The same sample, in colour, through the identical projection/tiling/placement path - so a layer's
// colour lands on the model exactly where its relief does, whatever projection it is using. Returns
// false (leaving `out` untouched) when the texture has no colour, or when the point falls outside a
// non-tiled placement, or behind a projective "from view" projector: all three mean "this layer does
// not colour this point", which is different from "this layer colours it black".
bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
const Vec3f &position, const Vec3f &normal, Vec3f &out,
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr);
// Area-weighted centroid and average normal of a layer's currently painted patch, in mesh-local
// coordinates - the same measurements build_texture_displacement() uses to pick its dominant
// projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
// wherever the layer is actually painted. Returns false (leaving the outputs untouched) if the
// layer has nothing painted yet.
bool compute_layer_paint_anchor(const indexed_triangle_set &base_mesh,
const TriangleSelector::TriangleSplittingData &facet_data,
Vec3f &anchor_pos,
Vec3f &anchor_normal);
// Extracts the currently painted patch from a volume's base mesh + stored facet data - the same
// extraction build_texture_displacement() and compute_layer_paint_anchor() each do internally via
// TriangleSelector::get_facets_strict(ENFORCER). Returns an empty mesh if nothing is painted.
// Exposed so GUI code (the LSCM "UV editor" preview pane) can get the same patch build_texture_
// displacement() would act on, without duplicating the deserialize/get_facets_strict boilerplate.
indexed_triangle_set extract_painted_patch(const indexed_triangle_set &base_mesh,
const TriangleSelector::TriangleSplittingData &facet_data);
// A patch flattened into 2D. The patch is first split into charts along edges sharper than
// `seam_angle_deg` (see LSCM_DEFAULT_SEAM_ANGLE_DEG), each chart is flattened independently, the
// charts are scaled to their true mm size and packed side by side.
//
// A vertex sitting on a seam belongs to several charts at once and therefore has a *different* UV
// in each of them, so this cannot be a plain "one UV per patch vertex" array: seam vertices are
// duplicated, once per chart touching them. `indices` is the patch's own triangle list re-indexed
// onto that duplicated vertex set, and `source_vertex` maps each duplicate back to the patch vertex
// it came from.
struct PatchUnwrap
{
std::vector<Vec2f> uvs; // one per unwrapped vertex, in mm
std::vector<int> source_vertex; // unwrapped vertex -> index into patch.vertices
std::vector<int> vertex_chart; // unwrapped vertex -> chart (island) id
std::vector<stl_triangle_vertex_indices> indices; // patch triangles, re-indexed into `uvs`
// Per chart, the centroid of its uvs - the point a TextureIsland's rotation turns about.
std::vector<Vec2f> chart_centroid;
// Edges belonging to exactly one triangle: the outline of each island. Indices into `uvs`. This
// is what the UV editor draws highlighted, so the boundaries the seam angle cut are visible.
std::vector<std::pair<int, int>> boundary_edges;
int chart_count = 0;
bool empty() const { return indices.empty(); }
};
// Applies an island's hand placement (scale + rotation about its own centroid, then offset) to one
// unwrapped UV. A chart with no entry in `islands` is left exactly where the packing put it.
Vec2f apply_island_transform(const Vec2f &uv, int chart, const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands);
// The same transform as a 2x3 affine matrix (columns: x basis, y basis, translation), for callers
// that would otherwise apply it to every vertex of an island one at a time. The UV editor renders
// each island through this as a uniform, which is what lets a drag move an island without touching
// its vertex buffer at all.
Eigen::Matrix<float, 2, 3> island_transform_matrix(int chart, const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands);
// Lays the unwrap's charts out as a connected net: charts that share a mesh edge are unfolded so
// their shared edge coincides (a cube -> its six faces joined along a spanning tree of edges, the rest
// left as free borders). Charts stay separate islands, so their borders still show and any of them can
// still be moved by hand afterwards. A chart whose unfold would overlap one already placed is left
// where the packing put it. Returns one placement per chart. See the gizmo's auto-connect option.
std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap);
// The placement that unfolds `child` onto `parent` along their shared mesh edge, honouring `parent`'s
// current placement in `islands`. Returns false if the two charts share no edge. Backs the manual
// "Join" command; compute_connected_net() does the same thing across a whole spanning tree.
bool join_chart_placement(const PatchUnwrap &unwrap, const std::vector<TextureIsland> &islands,
int child, int parent, TextureIsland &out_child);
// Unwraps `patch` as described above. Charts that are flat (within a degree) are projected onto
// their own tangent plane directly, which is both exact and far cheaper than a solve; only genuinely
// curved charts go through CGAL's LSCM parameterizer (MeshBoolean::cgal::parameterize_lscm()). A
// chart that LSCM cannot flatten at all (it is not a topological disk - closed, or with a hole)
// falls back to that same tangent-plane projection.
//
// `padding_mm` is the gap the packing leaves between islands; negative means auto (see
// TextureDisplacementLayer::island_padding_mm). `seam_edges` are extra edges to cut along regardless
// of angle (manual/auto seams), in the patch's own vertex-index space (which is the mesh's, since the
// patch carries the whole vertex array - see get_facets_strict()).
//
// Results are cached, keyed on the patch's geometry, the seam angle, the padding and the seam edges:
// nothing else about a layer (depth, tiling, rotation, offset, texture, island placement) changes the
// unwrap, so dragging any of those sliders must not pay for a re-solve.
PatchUnwrap compute_patch_unwrap(const indexed_triangle_set &patch, float seam_angle_deg = LSCM_DEFAULT_SEAM_ANGLE_DEG,
float padding_mm = -1.f, const std::vector<std::pair<int, int>> &seam_edges = {});
// One UV per patch vertex, for displacement. Displacement is inherently per-vertex - a vertex has
// exactly one position, so it can only be pushed out by one height - which means a seam vertex has
// to settle on a single one of its charts' UVs (the first, arbitrarily). That is not a compromise
// in the result: the surface stays watertight either way, since neighbouring vertices each move
// along their own normals and nothing depends on the UVs agreeing across the seam. It is only the
// *display* in the UV editor that needs the duplicated-vertex form above.
//
// Returns an empty vector if the patch has no triangles. Takes the whole layer because it applies
// both the layer's seam angle and its hand-placed islands.
std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
// One paint mask (as stored by ModelVolume::texture_displacement_facets) per possible layer slot.
using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplittingData, TEXTURE_DISPLACEMENT_MAX_LAYERS>;
// Bake all painted texture-displacement layers into `base_mesh`'s geometry, restricted to the
// painted area(s) only (the rest of the mesh is left untouched). Returns the mesh unchanged if
// nothing is painted or no layer has a usable texture.
//
// **Topology-preserving**: the returned mesh has exactly `base_mesh`'s vertices and triangles, in
// the same order - only the positions of displaced vertices differ. Every layer's paint mask is
// evaluated against `base_mesh` directly, and each vertex accumulates a single signed displacement
// (in mm) that all the layers covering it fold into, in slot order, via their TextureBlendMode.
// The vertex is then moved once, along its base-mesh normal, by that accumulated total.
//
// This replaced an earlier design that instead applied the layers *sequentially*, re-meshing after
// each one and carrying the next layer's paint mask onto the result with
// TriangleSelector::remap_painting(). That was the cause of a real "the second texture is never
// applied" bug: remapping a mask onto a mesh whose vertices had just been displaced out from under
// it routinely produced an empty bitstream, and the layer was then silently skipped. It is also
// what forced the per-layer vertex duplication and the final its_compactify_vertices() pass. The
// accumulate-then-displace formulation has neither problem, is substantially faster (no remap, no
// welding, one pass over the mesh), and - because the output keeps the input's exact vertex
// indexing - lets the GUI overlay a preview on the base mesh without any index translation.
//
// A vertex used by even one *unpainted* triangle of a layer's mask sits on that layer's boundary.
// Whether it moves is TextureDisplacementOptions::displace_border; see that field for why displacing
// it is safe (and the default). Either way the *direction* every vertex moves in is the area-weighted
// normal of the triangles that are painted in at least one layer - not of the whole mesh - so a
// border vertex travels along the painted surface's own normal instead of a blend with whatever
// unpainted geometry meets it there. Without that, the rim of a fully painted face would displace
// along the 45 degrees bisector it shares with the side wall and flare outwards. Interior vertices
// have every incident triangle painted, so for them the two are the same normal.
//
// Takes plain copied data rather than a ModelVolume reference so it is safe to call from a
// background thread (e.g. a bake Job's process() method) on a snapshot captured on the main
// thread, without touching the live Model concurrently with the UI.
//
// Known limitation: this does not attempt to remap texture-displacement paint data across
// topology-changing operations performed outside this gizmo (e.g. ModelObject::split(),
// mesh-boolean ops) the way TriangleSelector::remap_painting() does for the other paint channels.
// Such operations will silently drop any unbaked texture-displacement paint on the affected
// volume. This is an explicit extension point for a later phase, not an oversight.
//
// `progress`, when set, is called from the worker thread with a 0..100 completion percentage as the
// bake proceeds. Returning false from it aborts the run, which then returns an *empty* mesh - never
// a partially displaced one, so a cancelled bake can never be mistaken for a finished result and
// committed. It exists because this is the one call in the feature that can take seconds on a
// subdivided mesh, and without it the progress notification the Job framework puts on screen sits at
// 0% for the whole run and offers no way to close it (its close button only appears at 100%).
//
// `color`, when given, also reports which filament each triangle should print in - see
// TextureColorRequest.
struct TextureColorRequest
{
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
// of filaments actually loaded (see ColorQuantizeFn).
ColorQuantizeFn quantize;
// Palette index + position -> filament. Optional: without it a palette index is taken to be a
// filament index directly, which is the no-mixing case.
ColorResolveFn resolve;
// Majority-filter passes over the *perceived* colour, before any interleaving is resolved.
//
// Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own
// detail is finer than the mesh: two neighbouring facets land either side of some contour and flip
// colour independently. Replacing each facet's colour with the most common one among itself and
// its edge neighbours removes exactly that, and leaves any feature wider than a facet alone. 0
// turns it off.
int despeckle_passes = 0;
// Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned
// mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from
// the texture". The +1 is not arbitrary - it lines up with EnforcerBlockerType, where 0 is NONE
// ("use the volume's own filament") and 1..16 are Extruder1..16, so the caller can hand these
// straight to a TriangleSelector without a second mapping table.
std::vector<uint8_t> *out_triangle = nullptr;
};
// Where the volume sits on the plate: its instance transform times its own volume transform, i.e.
// mesh coordinates -> world millimetres.
//
// Every number the user sets is in real millimetres on the printed part - "Depth (mm)", "Tile size
// (mm)" - and the build plate is a world plane, so the bake runs in world space and transforms the
// result back. Doing it in the volume's own coordinates instead made a scaled instance stretch both
// the relief depth and the tiling by the scale factor, and under a non-uniform scale it also
// displaced along the wrong direction: a mesh normal maps to the world normal through the inverse
// transpose, not through the transform itself, so the relief leaned. Identity - the default - is
// exactly the old behaviour and is what an untransformed volume gives.
indexed_triangle_set build_texture_displacement(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
const TextureDisplacementOptions &options = {},
const DisplacementProgressFn &progress = {},
const TextureColorRequest *color = nullptr,
const Transform3d &volume_to_world = Transform3d::Identity());
// `volume`'s mesh coordinates -> world millimetres: its first instance's transform times its own.
// The mesh is shared by every instance, so a multi-instance object can only be baked for one of
// them; the first is what the gizmo edits against. Identity when the volume has no object yet.
Transform3d texture_displacement_volume_to_world(const ModelVolume &volume);
// Convenience overload for main-thread callers: extracts the mesh/layers/paint data/options from
// `volume` and forwards to the overload above.
indexed_triangle_set build_texture_displacement(const ModelVolume &volume);
// Laplacian relaxation of `mesh` in place, restricted to the vertices flagged in `movable` (sized to
// the mesh's vertex count; anything else is held exactly where it is and still acts as an anchor for
// its neighbours). Each of `iterations` passes moves a movable vertex a `strength` fraction of the
// way to the average of the vertices it shares an edge with, computed from the positions at the
// start of that pass so the result does not depend on vertex order.
//
// Topology-preserving like the bake itself, so it composes with it: this is what "smooth the relief
// after displacing it" runs, and it is also safe to run standalone on an already baked mesh.
// `strength` is clamped to [0, 1]; 0 iterations, an empty/mis-sized `movable`, or an all-false one
// leave the mesh untouched.
// `on_pass`, when set, is called with the 0-based index of each completed pass; returning false stops
// the relaxation there, leaving the passes already done in place.
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
int iterations, const DisplacementProgressFn &on_pass = {});
// Returns a scalar height (in mm - a displacement magnitude) at a surface point, given that point's
// position and interpolated normal. This is what feature-adaptive subdivision samples to decide
// where the displaced surface has *curvature* worth spending triangles on. Called serially from the
// subdivider, so it only needs to be safe on the calling thread.
using HeightFieldSampler = std::function<float(const Vec3f &pos, const Vec3f &normal)>;
// Builds a sampler of the *combined* (all-layers) displacement height in mm at an arbitrary surface
// point, for feature-adaptive subdivision. It mirrors build_texture_displacement()'s per-layer setup
// (decode, patch centroid, cylinder axis, blend order, "lowest layer folds additively") but evaluates
// per point instead of per vertex. Two deliberate simplifications, both erring toward *more* detail
// (safe - over-refinement is never a crack): every sampleable layer is evaluated at every point (no
// per-point paint-mask test, so a point sees all layers' textures, not only the ones painted there),
// and edge-smoothing's boundary falloff is ignored. LSCM layers have no per-point UV and are skipped.
// Returns a null sampler (bool false) when no layer can be sampled - the caller then falls back to
// uniform adaptive subdivision.
// Which filament the texture stack would put at a point, as a palette index (or -1 for "no colour
// here"). The colour analogue of HeightFieldSampler, and used the same way: to decide where the
// adaptive subdivision needs triangles. Colour lands per *facet*, so a colour boundary is a step the
// mesh can only draw if there are edges along it - the chord-error test that drives the height
// refinement is blind to it, exactly as it is blind to the paint's own border.
using ColorFieldSampler = std::function<int(const Vec3f &pos, const Vec3f &normal)>;
// The colour counterpart of make_combined_displacement_sampler(), over the same layers, and skipping
// the same ones (LSCM has no per-point UV). Layers without color_enabled, and layers whose texture is
// grayscale, contribute nothing; a higher slot wins over a lower one where they overlap, matching the
// bake. Returns null when no layer can colour anything, in which case there is nothing to refine for.
ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize);
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data);
// Uniformly subdivides `mesh` (every triangle recursively split into 4 via edge midpoints, using a
// shared cache so a midpoint is computed once and reused by both triangles on either side of that
// edge) until every edge is at or below max_edge_length_mm, or max_iterations passes have run,
// whichever comes first (bounding the worst-case triangle-count explosion on a very fine target).
//
// This exists so a low-poly input model can still get fine-grained texture displacement detail -
// build_texture_displacement() can only ever move existing vertices, so a patch with only a
// handful of vertices to begin with cannot show much detail no matter the texture's resolution.
//
// Deliberately whole-mesh and uniform, not limited to a painted patch: subdividing only part of a
// mesh while leaving the rest untouched creates a classic T-junction/cracking problem where the
// denser and sparser regions meet (the finer side has edge midpoints the coarser side doesn't
// know about). Uniform, whole-mesh subdivision has no such seam and stays manifold, at the cost of
// applying everywhere rather than just where texture detail is actually wanted - meant to be run
// once, deliberately, before painting (see the gizmo's "Subdivide model" button), not automatically
// during baking.
indexed_triangle_set subdivide_mesh_uniform(const indexed_triangle_set &mesh, float max_edge_length_mm, int max_iterations = 6);
// Adaptive subdivision by Rivara longest-edge bisection, restricted to a region.
//
// Unlike subdivide_mesh_uniform() this only densifies where asked - `refine_region` (indexed by
// input-triangle index; empty or all-false => no-op) flags the triangles allowed to drive refinement
// - so a small painted patch on a large model does not quadruple the whole model's triangle count.
// It is nonetheless *conformal*: it never leaves a T-junction/crack at the boundary between the
// refined and coarse regions (the trap that made subdivide_mesh_uniform() deliberately whole-mesh).
//
// A triangle wants refining while it is over at least one of these, whichever applies:
// - length: its longest edge exceeds `target_edge_length_mm` (a *baseline* - it applies in feature
// mode too, and is what stops a coarse triangle from being declared flat merely because
// the four points the chord test samples happened to land at similar heights on a
// high-frequency texture: the classic aliasing stall);
// - feature: (only when `sampler` is set and `chord_tolerance_mm > 0`) the *displaced* surface
// departs from the flat triangle by more than `chord_tolerance_mm`, measured as the max
// over the three edge midpoints AND the centroid of |sampled displacement - the flat
// triangle's barycentric interpolation|. Sampling the interior, not just edge midpoints,
// is what catches a bump that sits inside a triangle. This is a *curvature* test: it is
// exactly zero on a plane or a linear ramp (barycentric interpolation is exact there, so
// those stay coarse - the case a gradient criterion would over-refine) and large on a
// bump/ridge/noise.
// `min_edge_length_mm` is a hard floor under both: no triangle whose longest edge is already at or
// below it is ever refined, which is also what guarantees termination across a sharp texture step
// (where the chord error never falls below the tolerance no matter how fine the mesh gets).
//
// Refinement runs to completion, not for a fixed number of passes: triangles are taken worst-first
// from a max-heap keyed by how many times over its criteria each one is, so a run that hits the
// `max_triangles` budget has spent it on the largest errors rather than wherever a sweep happened to
// reach. The budget is the only bound on a pathological height field; stopping on it leaves a
// perfectly valid, still-conformal mesh.
//
// How it stays crack-free: only "terminal" edges are ever bisected - an edge that is the longest edge
// of *both* triangles sharing it (or a boundary edge that is the longest of its one triangle).
// Bisecting such an edge splits both its triangles 1->2 around the same new midpoint, so a hanging
// node is never created. The edge to split for a triangle that wants refining is found by Rivara
// longest-edge propagation (LEPP): walk to the longest edge of ever-longer-edged neighbours until a
// terminal edge is reached, and bisect that. Edge length strictly increases along the path (ties
// broken by a mesh-vertex key, which both sides of an edge compute identically), so the walk cannot
// cycle, and Rivara's result is that repeating it refines the original triangle in a bounded number
// of bisections - closing the propagation gap a plain per-edge test leaves behind. The transition
// triangles this pulls in just outside the region are the graded band that makes the size change
// conformal; they are a bounded cost paid once, not a per-pass tax.
//
// If `out_source` is non-null it is resized to the output triangle count and out_source[i] receives
// the input triangle that output triangle i descends from (children inherit their parent's index), so
// a caller can carry per-triangle data - e.g. a paint mask - across the topology change without a
// geometric remap.
//
// `refine_region` is a **bitmask** per input triangle, not a plain flag:
// bit 0 (REFINE_PAINTED) - inside the painted area: refine by the length baseline and, in feature
// mode, by the chord-error test.
// bit 1 (REFINE_BORDER) - inside the band straddling the paint's edge: refine by
// `border_edge_length_mm` alone.
// A value of 1 therefore means exactly what a plain 1 always meant, and 0 still means "never touch
// this triangle except through the conformal closure".
//
// The border band exists because the chord-error test is blind to the one discontinuity the bake
// actually creates. `make_combined_displacement_sampler()` evaluates the height field everywhere,
// with no per-point paint test, so where the paint *stops* it keeps reporting full relief - smooth
// and low-curvature - while the baked surface steps from full displacement to zero. The test sees no
// error there and leaves the transition at whatever density the input had, which is what turns the
// rim of an unpainted island into a ring of large, steeply tilted triangles. Refining that band by
// plain edge length is bounded (it is a thin ring, and a length target always terminates) and needs
// no paint-aware sampler.
//
// `color`, with a positive `color_edge_length_mm`, adds a third criterion inside the painted area: a
// triangle whose corners, edge midpoints and centroid do not all map to the *same* filament straddles
// a colour boundary, and is refined by plain edge length down to that target. Length rather than any
// error measure, for the same reason the border band uses length - the thing being fixed is the size
// of the triangles spanning a step, not the curvature of anything - and because a step's error never
// falls however fine the mesh gets, so only a length target (floored by min_edge_length_mm) is
// guaranteed to terminate. Without this a colour boundary lands on whatever triangles the *height*
// happened to need, which on a flat surface is none at all.
//
// `progress`, when given, is called with a 0..100 percentage of the triangle budget spent; returning
// false stops the refinement early. What it hands back then is still a complete, conformal mesh - the
// loop only ever finishes whole bisections - so a caller that wants to discard it has to do so itself.
indexed_triangle_set subdivide_mesh_adaptive(const indexed_triangle_set &mesh,
const std::vector<uint8_t> &refine_region,
float target_edge_length_mm, int max_triangles = 1000000,
std::vector<int> *out_source = nullptr,
const HeightFieldSampler &sampler = nullptr,
float chord_tolerance_mm = 0.f, float min_edge_length_mm = 0.f,
float border_edge_length_mm = 0.f,
const DisplacementProgressFn &progress = nullptr,
const ColorFieldSampler &color = nullptr,
float color_edge_length_mm = 0.f);
// The recipe for getting a mesh ready to receive displacement: even out the triangle density, then
// refine it where the texture bends. Either stage is skipped when its target is <= 0. Pure data, and
// the whole of it, so the preparation can be handed to a background job instead of running on the UI
// thread - see GLGizmoTextureDisplacement::prepare_mesh().
struct TextureDisplacementPrepareParams
{
// Isotropic remesh (CGAL). The target is clamped against the part's own surface area before it is
// used, so a value that would produce millions of triangles cannot be asked for by accident.
float remesh_edge_mm = 0.f;
float remesh_sharp_deg = 0.f; // 0 = do not protect sharp edges
// Adaptive (Rivara) subdivision of the painted area. See subdivide_mesh_adaptive().
float subdiv_target_mm = 0.f; // "Max edge": the length baseline, and the only criterion when
// subdiv_feature is off
float subdiv_detail_mm = 0.f; // "Detail": chord tolerance, feature mode only
float subdiv_min_edge_mm = 0.f; // "Min edge": the floor under both, feature mode only
float subdiv_border_mm = 0.f; // "Edge detail": the band straddling the paint's edge, 0 = off
bool subdiv_feature = false; // follow texture curvature, not just edge length
int subdiv_added_triangles = 0; // budget, *added* to the mesh's own count
// Edge length triangles straddling a *colour* boundary are refined to, 0 = do not look at colour.
// Separate from the height criteria because colour lands per facet: a flat surface carrying a
// sharp colour edge needs triangles along that edge even though its height is perfectly smooth.
float subdiv_color_edge_mm = 0.f;
};
// What a preparation run produced. An empty `mesh` means there was nothing to do and the caller must
// commit nothing - which is not a failure: a mesh that is already even needs no remesh, and one that
// is already fine enough for the texture needs no subdivision.
struct TextureDisplacementPrepareResult
{
indexed_triangle_set mesh;
TextureDisplacementFacetsData masks;
// The remesh landed but no layer's paint survived being carried onto it. Nothing is committed:
// everything downstream is driven by that paint, so baking on would bake a flat mesh.
bool paint_lost = false;
};
} // namespace Slic3r
#endif // slic3r_TextureDisplacement_hpp_

View File

@@ -1519,9 +1519,11 @@ void TriangleSelector::get_facets(std::vector<indexed_triangle_set>& facets_per_
}
}
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state) const
indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source) const
{
indexed_triangle_set out;
if (out_source)
out_source->clear();
size_t num_vertices = 0;
for (const Vertex &v : m_vertices)
@@ -1535,8 +1537,13 @@ indexed_triangle_set TriangleSelector::get_facets_strict(EnforcerBlockerType sta
out.vertices.emplace_back(v.v);
}
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle)
for (int itriangle = 0; itriangle < m_orig_size_indices; ++ itriangle) {
this->get_facets_strict_recursive(m_triangles[itriangle], m_neighbors[itriangle], state, out.indices);
// Everything the recursion just appended came from this original triangle, whatever depth it
// was split to. Recording it here keeps the recursive helpers untouched.
if (out_source)
out_source->resize(out.indices.size(), itriangle);
}
for (auto &triangle : out.indices)
for (int i = 0; i < 3; ++ i)

View File

@@ -350,7 +350,14 @@ public:
// Get facets at a given state. Don't triangulate T-joints.
indexed_triangle_set get_facets(EnforcerBlockerType state) const;
// Get facets at a given state. Triangulate T-joints.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state) const;
// Sub-triangles in `state`, with the *whole* mesh's referenced vertex array (only .indices is
// filtered by state, so two calls with different states share one indexing).
//
// `out_source`, when given, is filled parallel to the returned .indices with the index of the
// original mesh triangle each sub-triangle came from. That is what lets a caller carry partial
// paint - the pieces of a triangle a brush stroke only partly covered - across a refinement of
// the same surface, instead of having to round each source triangle to wholly painted or not.
indexed_triangle_set get_facets_strict(EnforcerBlockerType state, std::vector<int> *out_source = nullptr) const;
// Get edges around the selected area by seed fill.
std::vector<Vec2i32> get_seed_fill_contour() const;

View File

@@ -200,6 +200,8 @@ set(SLIC3R_GUI_SOURCES
GUI/Gizmos/GLGizmosManager.hpp
GUI/Gizmos/GLGizmoSVG.cpp
GUI/Gizmos/GLGizmoSVG.hpp
GUI/Gizmos/GLGizmoTextureDisplacement.cpp
GUI/Gizmos/GLGizmoTextureDisplacement.hpp
GUI/Gizmos/GLGizmoUtils.cpp
GUI/Gizmos/GLGizmoUtils.hpp
#GUI/Gizmos/GLGizmoText.cpp
@@ -325,6 +327,12 @@ set(SLIC3R_GUI_SOURCES
GUI/Jobs/SLAImportDialog.hpp
GUI/Jobs/SLAImportJob.cpp
GUI/Jobs/SLAImportJob.hpp
GUI/Jobs/TextureDisplacementBakeJob.cpp
GUI/Jobs/TextureDisplacementBakeJob.hpp
GUI/Jobs/TextureDisplacementPrepareJob.cpp
GUI/Jobs/TextureDisplacementPrepareJob.hpp
GUI/Jobs/TextureDisplacementPreviewJob.cpp
GUI/Jobs/TextureDisplacementPreviewJob.hpp
GUI/Jobs/ThreadSafeQueue.hpp
GUI/Jobs/UpgradeNetworkJob.cpp
GUI/Jobs/UpgradeNetworkJob.hpp
@@ -506,6 +514,10 @@ set(SLIC3R_GUI_SOURCES
GUI/TaskManager.hpp
GUI/TextLines.cpp
GUI/TextLines.hpp
GUI/TextureLibrary.cpp
GUI/TextureLibrary.hpp
GUI/TextureProjectorFrame.cpp
GUI/TextureProjectorFrame.hpp
GUI/TickCode.cpp
GUI/TickCode.hpp
GUI/TroubleshootDialog.cpp
@@ -522,6 +534,8 @@ set(SLIC3R_GUI_SOURCES
GUI/UserManager.hpp
GUI/UserNotification.cpp
GUI/UserNotification.hpp
GUI/UVEditorCanvas.cpp
GUI/UVEditorCanvas.hpp
GUI/WebDownPluginDlg.cpp
GUI/WebDownPluginDlg.hpp
GUI/WebGuideDialog.cpp
@@ -915,18 +929,6 @@ if (APPLE)
endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
elseif (WIN32)
# Prebuilt shared FFmpeg from the deps install. Windows has no pkg-config,
# so resolve the import libraries out of the deps prefix directly; the DLLs
# are copied next to the executable by the top level CMakeLists.
find_library(LIBAVCODEC_LIBRARY NAMES avcodec PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBSWSCALE_LIBRARY NAMES swscale PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
find_library(LIBAVUTIL_LIBRARY NAMES avutil PATHS ${CMAKE_PREFIX_PATH}/lib NO_DEFAULT_PATH)
if (NOT LIBAVCODEC_LIBRARY OR NOT LIBSWSCALE_LIBRARY OR NOT LIBAVUTIL_LIBRARY)
message(FATAL_ERROR "FFmpeg (avcodec/swscale/avutil) not found under ${CMAKE_PREFIX_PATH}/lib. Rebuild the deps.")
endif ()
target_link_libraries(libslic3r_gui ${LIBAVCODEC_LIBRARY} ${LIBSWSCALE_LIBRARY} ${LIBAVUTIL_LIBRARY})
target_include_directories(libslic3r_gui SYSTEM PRIVATE ${CMAKE_PREFIX_PATH}/include)
else ()
pkg_check_modules(LIBAV REQUIRED IMPORTED_TARGET
libavcodec

View File

@@ -55,6 +55,7 @@ private:
Label* m_text_label;
wxStaticBitmap* m_icon_bitmap;
int m_target_size;
std::string m_icon_name;
ScalableBitmap m_icon;
};

View File

@@ -60,6 +60,7 @@ class AboutDialog : public DPIDialog
wxHtmlWindow* m_html;
wxStaticBitmap* m_logo;
int m_copy_rights_btn_id { wxID_ANY };
int m_copy_version_btn_id { wxID_ANY };
public:
AboutDialog();

View File

@@ -93,6 +93,7 @@ private:
CenteredTitle* m_title_ctrl { nullptr };
wxString m_titleText;
wxAuiToolBarItem* m_account_item;
wxAuiToolBarItem* m_model_store_item;
//wxAuiToolBarItem *m_publish_item;

View File

@@ -74,10 +74,12 @@ private:
std::unordered_set<std::string> m_system_filament_types_set;
std::set<std::string> m_visible_printers;
CreateType m_create_type;
Button * m_button_create = nullptr;
Button * m_button_cancel = nullptr;
ComboBox * m_filament_vendor_combobox = nullptr;
::CheckBox * m_can_not_find_vendor_checkbox = nullptr;
ComboBox * m_filament_type_combobox = nullptr;
ComboBox * m_exist_vendor_combobox = nullptr;
ComboBox * m_filament_preset_combobox = nullptr;
TextInput * m_filament_custom_vendor_input = nullptr;
wxGridSizer * m_filament_presets_sizer = nullptr;

View File

@@ -16,27 +16,24 @@ namespace Slic3r
// This block is never executed at runtime.
static void _toolhead_translation_markers()
{
// Possible runtime values of tool_head_display_names, marked for extraction.
static const char *const markers[] = {
L("Main Extruder"), L("Main extruder"), L("main extruder"),
L("Auxiliary Extruder"), L("Auxiliary extruder"), L("auxiliary extruder"),
L("Left Extruder"), L("Left extruder"), L("left extruder"),
L("Right Extruder"), L("Right extruder"), L("right extruder"),
L("Main Nozzle"), L("Main nozzle"), L("main nozzle"),
L("Auxiliary Nozzle"), L("Auxiliary nozzle"), L("auxiliary nozzle"),
L("Left Nozzle"), L("Left nozzle"), L("left nozzle"),
L("Right Nozzle"), L("Right nozzle"), L("right nozzle"),
L("Main Hotend"), L("Main hotend"), L("main hotend"),
L("Auxiliary Hotend"), L("Auxiliary hotend"), L("auxiliary hotend"),
L("Left Hotend"), L("Left hotend"), L("left hotend"),
L("Right Hotend"), L("Right hotend"), L("right hotend"),
// Dynamic toolhead display names from JSON config — xgettext cannot scan these
L("Main Extruder"); L("Main extruder"); L("main extruder");
L("Auxiliary Extruder"); L("Auxiliary extruder"); L("auxiliary extruder");
L("Left Extruder"); L("Left extruder"); L("left extruder");
L("Right Extruder"); L("Right extruder"); L("right extruder");
L("Main Nozzle"); L("Main nozzle"); L("main nozzle");
L("Auxiliary Nozzle"); L("Auxiliary nozzle"); L("auxiliary nozzle");
L("Left Nozzle"); L("Left nozzle"); L("left nozzle");
L("Right Nozzle"); L("Right nozzle"); L("right nozzle");
L("Main Hotend"); L("Main hotend"); L("main hotend");
L("Auxiliary Hotend"); L("Auxiliary hotend"); L("auxiliary hotend");
L("Left Hotend"); L("Left hotend"); L("left hotend");
L("Right Hotend"); L("Right hotend"); L("right hotend");
// standalone position words (short_name=true runtime results)
L("main"), L("auxiliary"),
L("Main"), L("Auxiliary"),
L("left"), L("right"),
L("Left"), L("Right"),
};
(void) markers;
L("main"); L("auxiliary");
L("Main"); L("Auxiliary");
L("left"); L("right");
L("Left"); L("Right");
}
std::string DevPrinterConfigUtil::m_resource_file_path = "";

View File

@@ -199,6 +199,7 @@ private:
void OnRefreshButton(wxCommandEvent& event);
private:
int saveTimes{0};
wxBoxSizer* mainSizer{nullptr};
wxPanel* textPanel{nullptr};
wxBoxSizer* textSizer{nullptr};

View File

@@ -122,6 +122,7 @@ private:
private:
int m_ext_nozzle_id = -1;
int m_rack_nozzle_id = -1;
bool m_isRefreshFinish = false;
bool findNozzleImage = false;
NozzleStatus m_nozzle_status = NOZZLE_STATUS_DC;
@@ -153,6 +154,7 @@ private:
Label* m_diameter_label;
Label* m_flowtype_label;
Label* m_type_label;
ScalableButton* m_error_button{ nullptr };
Label* m_sn_label;
Label* m_version_label;

View File

@@ -266,7 +266,7 @@ void wgtDeviceNozzleRackSelect::OnNozzleItemSelected(wxCommandEvent &evt)
}
auto *item = dynamic_cast<wgtDeviceNozzleRackNozzleItem *>(evt.GetEventObject());
if (auto ptr = m_nozzle_rack.lock(); item && ptr) {
if (item; auto ptr = m_nozzle_rack.lock()) {
int to_select_pos_id = sGetNozzlePosId(item, m_toolhead_nozzle_l, m_toolhead_nozzle_r);
if (to_select_pos_id > -1 && to_select_pos_id != GetSelectedNozzlePosID()) {
SetSelectedNozzle(ptr->GetNozzleSystem()->GetNozzleByPosId(to_select_pos_id));

View File

@@ -72,6 +72,7 @@ public:
float m_model_z_offset{ 0.5f };
bool m_visible{ true };
bool m_is_dark = false;
bool m_fixed_screen_size{ false };
float m_scale_factor{ 1.0f };
#if ENABLE_ACTUAL_SPEED_DEBUG
ActualSpeedImguiWidget m_actual_speed_imgui_widget;

View File

@@ -104,6 +104,11 @@ std::pair<bool, std::string> GLShadersManager::init()
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" }, { "FLIP_TRIANGLE_NORMALS"sv });
else
valid &= append_shader("mm_gouraud", { prefix + "mm_gouraud.vs", prefix + "mm_gouraud.fs" });
// Fast bump-map preview for the texture displacement gizmo (see libslic3r/TextureDisplacement.hpp).
valid &= append_shader("texture_displacement_bump", { prefix + "texture_displacement_bump.vs", prefix + "texture_displacement_bump.fs" });
// UV-check overlay for the same gizmo: a procedural checker or a distortion heatmap over the
// painted patch, to sanity-check the unwrap.
valid &= append_shader("texture_displacement_uvcheck", { prefix + "texture_displacement_uvcheck.vs", prefix + "texture_displacement_uvcheck.fs" });
return { valid, error };
}

View File

@@ -171,7 +171,8 @@ bool GLTexture::load_from_svg_file(const std::string& filename, bool use_mipmaps
return false;
}
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy)
bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy,
bool use_mipmaps)
{
m_width = w;
m_height = h;
@@ -195,18 +196,51 @@ bool GLTexture::load_from_raw_data(std::vector<unsigned char> data, unsigned int
glsafe(::glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)m_width, (GLsizei)m_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
bool use_mipmaps = true;
if (use_mipmaps) {
// we manually generate mipmaps because glGenerateMipmap() function is not reliable on all graphics cards
int lod_w = m_width;
int lod_h = m_height;
// We generate the mipmap chain ourselves rather than calling glGenerateMipmap(), which this
// codebase has historically considered unreliable on some graphics cards.
//
// Each level is a 2x2 box filter of the level above it. Note this used to re-upload the
// *level-0* buffer at every level instead, which does not downscale anything - it just
// reinterprets the image's first lod_w * lod_h texels as the whole smaller level, i.e. every
// level below 0 held a crop of the top-left corner. It went unnoticed for as long as every
// caller drew these textures at roughly their native size (where only level 0 is ever
// sampled); it shows up the moment one is drawn small enough to select a lower level, as a
// texture that visibly turns into something else as it shrinks.
std::vector<unsigned char> scratch;
const std::vector<unsigned char> *src = &data;
int src_w = m_width;
int src_h = m_height;
GLint level = 0;
while (lod_w > 1 || lod_h > 1) {
while (src_w > 1 || src_h > 1) {
++level;
lod_w = std::max(lod_w / 2, 1);
lod_h = std::max(lod_h / 2, 1);
n_pixels = lod_w * lod_h;
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)data.data()));
const int lod_w = std::max(src_w / 2, 1);
const int lod_h = std::max(src_h / 2, 1);
std::vector<unsigned char> lod(size_t(lod_w) * size_t(lod_h) * 4);
for (int y = 0; y < lod_h; ++y) {
// min() rather than a plain 2*y+1: an odd source extent leaves the last output texel
// with only one source row/column to average, not two.
const int y0 = std::min(2 * y, src_h - 1);
const int y1 = std::min(2 * y + 1, src_h - 1);
for (int x = 0; x < lod_w; ++x) {
const int x0 = std::min(2 * x, src_w - 1);
const int x1 = std::min(2 * x + 1, src_w - 1);
for (int c = 0; c < 4; ++c) {
const unsigned int sum = (*src)[(size_t(y0) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y0) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x0)) * 4 + size_t(c)] +
(*src)[(size_t(y1) * size_t(src_w) + size_t(x1)) * 4 + size_t(c)];
lod[(size_t(y) * size_t(lod_w) + size_t(x)) * 4 + size_t(c)] = (unsigned char)(sum / 4);
}
}
}
glsafe(::glTexImage2D(GL_TEXTURE_2D, level, GL_RGBA, (GLsizei)lod_w, (GLsizei)lod_h, 0, GL_RGBA, GL_UNSIGNED_BYTE, (const void*)lod.data()));
scratch = std::move(lod);
src = &scratch;
src_w = lod_w;
src_h = lod_h;
}
glsafe(::glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, level));

View File

@@ -100,7 +100,10 @@ namespace GUI {
bool load_from_file(const std::string& filename, bool use_mipmaps, ECompressionType compression_type, bool apply_anisotropy);
bool load_from_svg_file(const std::string& filename, bool use_mipmaps, bool compress, bool apply_anisotropy, unsigned int max_size_px);
//BBS load GLTexture from raw pixel data
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false);
// `data` is RGBA, w * h * 4 bytes. With use_mipmaps, a real box-filtered mipmap chain is
// built, so the texture may safely be drawn smaller than its pixel size.
bool load_from_raw_data(std::vector<unsigned char> data, unsigned int w, unsigned int h, bool apply_anisotropy = false,
bool use_mipmaps = true);
// meanings of states: (std::pair<int, bool>)
// first field (int):
// 0 -> no changes

View File

@@ -327,6 +327,7 @@ private:
GLTexture m_icons_texture;
bool m_icons_texture_dirty;
mutable GLTexture m_images_texture;
mutable bool m_images_texture_dirty;
BackgroundTexture m_background_texture;
GLTexture m_arrow_texture;
Layout m_layout;

View File

@@ -86,6 +86,7 @@ private:
boost::thread m_thread;
// Mutex and condition variable to synchronize m_thread with the UI thread.
std::mutex m_mutex;
int m_generate_count;
// This map holds all translated description texts, so they can be easily referenced during layout calculations
// etc. When language changes, GUI is recreated and this class constructed again, so the change takes effect.

View File

@@ -27,7 +27,8 @@ enum class PainterGizmoType {
FDM_SUPPORTS,
SEAM,
MM_SEGMENTATION,
FUZZY_SKIN
FUZZY_SKIN,
TEXTURE_DISPLACEMENT
};
class TriangleSelectorGUI : public TriangleSelector {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,807 @@
#ifndef slic3r_GLGizmoTextureDisplacement_hpp_
#define slic3r_GLGizmoTextureDisplacement_hpp_
#include "GLGizmoPainterBase.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "slic3r/GUI/GLModel.hpp"
#include "slic3r/GUI/GLTexture.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/IconManager.hpp"
#include "slic3r/GUI/TextureLibrary.hpp"
#include <array>
#include <atomic>
#include <map>
#include <memory>
#include <string>
namespace Slic3r::GUI {
class TextureProjectorFrame;
// Paint-style gizmo that assigns one or more texture-displacement "layers" (see
// libslic3r/TextureDisplacement.hpp) to painted areas of a model, and can bake the result into
// real mesh geometry. See the project plan for the overall architecture; in short:
// - each layer owns its own independent paint mask (ModelVolume::texture_displacement_facets),
// reusing the same TriangleSelector/FacetsAnnotation machinery as every other paint gizmo -
// only one layer is "active" (paintable) at a time, selected in the panel below;
// - "Bake" runs build_texture_displacement() in a background job and commits the result exactly
// like the Emboss/SVG "project on surface" gizmo does.
class GLGizmoTextureDisplacement : public GLGizmoPainterBase
{
public:
GLGizmoTextureDisplacement(GLCanvas3D& parent, const std::string& icon_filename, unsigned int sprite_id);
// The whole of mesh preparation - remesh, carry the paint across, refine where the texture bends -
// as one pure function over plain data: no ModelVolume, no Model, no GUI, no undo. That is what lets
// TextureDisplacementPrepareJob run it on the job worker instead of on the UI thread, where CGAL's
// remesher and a several-hundred-thousand-triangle refinement together freeze the window for tens
// of seconds with nothing to look at and no way to cancel.
//
// `progress` is called with 0..100 and aborts the run when it returns false; an aborted run reports
// an empty result. An empty result is also how "nothing needed doing" is reported - see
// TextureDisplacementPrepareResult.
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementPrepareParams &params,
const std::vector<PrintableColor> &palette,
const DisplacementProgressFn &progress);
// The volume's eight texture-displacement masks, gathered into the array every pure function here
// (and every job input) takes.
static TextureDisplacementFacetsData facets_data_of(const ModelVolume &mv);
using PaletteEntry = PrintableColor;
// The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can
// address), plus - when `mixing` - every pair of them at evenly spaced ratios.
//
// Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too
// finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and
// blue banded together read as a desaturated grey-green, and that is what the preview must promise
// - blending them subtractively would show a green the printer cannot produce this way.
//
// How many ratios depends on how many filaments there are, so the palette stays bounded: the
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
// filaments there are already plenty of colours without mixing any of them.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing);
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
// a grey of similar brightness).
//
// Precomputed into a lookup cube rather than matched per call: the subdivision's colour criterion
// samples up to seven points per triangle and re-samples both children of every split, so a live
// match would dominate the refinement. The returned closure owns the cube, so it is safe to hand
// to a worker thread and outlives the palette it was built from.
static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette);
// Turns a palette index plus a position into the filament to print there, interleaving the two
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither
// cells. See ColorResolveFn for why this is separate from the quantizer.
static ColorResolveFn make_mix_resolver(const std::vector<PaletteEntry> &palette, ColorMixMode mode,
float layer_height, float cell_mm);
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
// height, despeckle. Empty when no layer is actually colouring.
TextureColorSettings color_settings_for(const ModelVolume &mv);
// The printable palette for the current filaments and mixing setting, rebuilt only when either
// actually changes - see the definition for why that caching is not optional.
const std::vector<PaletteEntry> &cached_palette();
std::vector<PaletteEntry> m_palette_cache;
std::vector<ColorRGBA> m_palette_filaments;
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer;
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
static std::vector<ColorRGBA> filament_palette();
// The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be
// read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything.
static float print_layer_height();
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer
// sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex
// colour attribute, and so no change to GLModel's vertex layouts.
//
// The *index buffer* is what gets reordered, never m_preview_its: the paint overlay and the
// wireframe index into that by the volume's own triangle numbering (the bake is
// topology-preserving), and permuting it would silently misplace both.
struct PreviewColorRun
{
std::pair<size_t, size_t> range; // into the GLModel's index buffer, in elements
ColorRGBA color;
};
std::vector<PreviewColorRun> m_preview_color_runs;
// True if any of the volume's layers would actually colour something: colour turned on, and a
// texture that has colour to give. What decides whether a palette is captured into a job at all,
// and so whether the colour criterion and the mmu write ever run.
static bool any_layer_colors(const ModelVolume &mv);
void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
// rotation handles instead of painting); otherwise forwards to the normal painting handling.
bool on_mouse(const wxMouseEvent &mouse_event) override;
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
std::string get_gizmo_entering_text() const override { return _u8L("Entering Texture displacement painting"); }
std::string get_gizmo_leaving_text() const override { return _u8L("Leaving Texture displacement painting"); }
std::string get_action_snapshot_name() const override { return _u8L("Texture displacement editing"); }
EnforcerBlockerType get_left_button_state_type() const override { return EnforcerBlockerType::ENFORCER; }
EnforcerBlockerType get_right_button_state_type() const override { return EnforcerBlockerType::NONE; }
private:
bool on_init() override;
void update_model_object() override;
void update_from_model_object(bool first_update) override;
void on_opening() override {}
void on_shutdown() override;
PainterGizmoType get_painter_type() const override;
// Phase 1 restricts the texture layer list to the first model-part volume of the current
// object (the common single-volume case); multi-part objects only get texture layers on
// their first part until a later phase. Returns nullptr if there is no model part.
ModelVolume* texture_volume();
const ModelVolume* texture_volume() const;
void add_texture_layer();
void remove_texture_layer(int slot);
void set_active_layer(int slot); // flushes the previous layer's edits, then reloads selectors
// `own_snapshot` false when the caller has already taken an undo step that is meant to cover the
// displacement too - see bake_standard().
void bake(bool own_snapshot = true);
// Standard (0) vs Pro (1), driven by the two-position slider in the panel header.
//
// Pro is the panel as it has always been: every geometry-preparation control is visible and the
// user drives Remesh, Subdivide and Bake themselves, in whatever order they like. Standard hides
// all of that, pins it to one fixed recipe, and folds it into the Bake button - paint, press Bake,
// done - so the common case does not require knowing that a height map can only move vertices that
// already exist. Nothing about Pro changed when Standard was added.
int m_panel_mode = 0;
bool pro_mode() const { return m_panel_mode != 0; }
// Pins every control Standard mode hides to its preset value. Idempotent, called each frame while
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
// changed something, so the caller can invalidate the preview.
bool apply_standard_mode_presets(ModelVolume *mv);
// Standard mode's Bake: remesh to an even density, refine where the texture bends, then displace.
// The order matters and is the whole reason this is one button - a height map can only move
// existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
// the paint if it were not carried across (see prepare_mesh()).
void bake_standard();
// Queues one prepare_mesh() run on the job worker and commits its result when it lands. Every
// mesh-preparation button goes through here - Pro's Remesh, Pro's adaptive Subdivide and Standard's
// Bake differ only in which stages `params` enables and in what happens afterwards:
// - `snapshot_name` is the single undo step the commit opens. With `then_bake` it is also the step
// the displacement job that follows commits into, rather than pushing its own - an undo landing
// between the two would leave a mesh carrying every added triangle and no relief on it, and
// baking again from there would prepare it a second time.
// - `unchanged_msg`, when not empty, is shown if the run had nothing to do. Standard's Bake passes
// nothing: a mesh that already meets the criteria is not an error there, it just goes straight
// on to the displacement.
void queue_prepare(const TextureDisplacementPrepareParams &params, const std::string &snapshot_name,
bool then_bake, const std::string &unchanged_msg);
// Set from queue_prepare() until its job's result has been committed. Distinct from
// m_bake_in_progress because Standard's Bake sets both in turn, and because every button that would
// read or replace the mesh has to stay disabled for the whole of it.
bool m_prepare_in_progress = false;
// How one layer's paint sits on the pre-subdivision mesh, precise enough to carry across the
// refinement without rounding each source triangle to wholly painted or not.
//
// Rounding is what made the outline of a painted region come out ragged: a source triangle near a
// smooth brush boundary is wholly painted essentially at random, so "painted iff the source was
// full" turns a clean curve into a noisy fringe of isolated painted and unpainted triangles - and
// once the border band refines the mesh there, that fringe is reproduced faithfully instead of
// being blurred away by coarse geometry.
struct LayerPaintMap
{
std::vector<uint8_t> full; // per source triangle: covered edge to edge
std::vector<int> part_start; // CSR offsets into `part`, size (source tris + 1)
std::vector<std::array<Vec3f, 3>> part; // painted pieces of partly covered source triangles
bool empty() const { return full.empty(); }
};
// Rebuilds every layer's mask on a subdivided mesh from `source` (new triangle -> the input triangle
// it descends from) and the pre-subdivision coverage in `paint`.
static TextureDisplacementFacetsData masks_after_subdivision(
const TriangleMesh &new_mesh, const std::vector<int> &source,
const std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> &paint);
// False means the remesh failed or changed nothing (CGAL signals failure by handing the input back),
// and `out` must not be used. `target_edge_mm` is a request rather than a promise: it is clamped
// against the part's surface area first, because CGAL's cost grows with the square of 1/target.
static bool plan_remesh(const indexed_triangle_set &src, float target_edge_mm, float sharp_angle_deg,
indexed_triangle_set &out);
// Marks every facet of every model-part volume as painted for the currently active layer -
// "whole model" as an alternative to brushing/clicking every triangle by hand.
void select_whole_model();
// The mesh raycasters are built one per model-part volume, in that order; this is the texture
// volume's slot among them, or -1 if it has none (no selection, or the lists disagree).
int texture_volume_raycaster_index() const;
// Paints exactly the facets currently visible from the camera onto the active layer, replacing
// whatever that layer had painted. "Visible" is two tests: the facet faces the camera, and its
// centroid is not hidden behind other geometry (a real raycast, so a concave part's far inner
// wall is correctly excluded). When `uv_clip` is given (the projection frame's matrix), facets
// whose centroid falls outside the frame's uv unit square are skipped first - which both clips
// the selection to the frame and spares the raycast for everything outside it. Costs one ray
// query per surviving facet, so it is a one-shot action, never a per-frame one. Returns the
// number of facets selected.
int select_visible_faces(const std::array<float, 12> *uv_clip = nullptr);
// When set, "Capture current view" also re-selects the visible faces, so the viewpoint the
// projector was captured from and the area it projects onto stay the same. Independent of the
// projection frame below: this takes every visible facet, the frame clips to its rectangle.
// Off by default, because turning it on replaces whatever the layer had painted.
bool m_project_only_visible = false;
// The projection-frame overlay for a ViewProjected layer: a semi-transparent window dragged over
// the 3D view whose border becomes the projection's edge. Created lazily and owned here; hidden
// rather than destroyed when closed, so reopening keeps it where the user left it.
TextureProjectorFrame *m_projector_frame = nullptr;
int m_projector_opacity = 140;
// What the overlay's texture was last built from, so repeated updates don't rebuild the bitmap
// from unchanged pixels. Same shape as the m_thumbnail_source/m_thumbnail_smoothing pair above.
const void *m_projector_tex_source = nullptr;
float m_projector_tex_smoothing = -1.f;
void show_projector(bool show);
// Pushes the active layer's texture into the overlay. Cheap, and a no-op while it is hidden.
void update_projector();
// Reads the overlay's rectangle and commits it as the layer's projection: builds the exact
// projective local->uv matrix from the camera and that rectangle, turns tiling off so the border
// is a hard edge, and repaints the layer with the visible facets inside the frame. Returns the
// number of facets selected, or -1 if the frame could not be used at all.
int apply_projection_frame();
// Uniformly subdivides the volume's mesh (see libslic3r::subdivide_mesh_uniform()) so a
// low-poly input model has enough vertices to actually show texture-displacement detail.
// A real, committed geometry change (like Bake), so it needs its own snapshot; unlike Bake it
// has no target region, so any not-yet-baked paint on the volume is dropped rather than
// remapped (texture-displacement paint has no remap-across-topology-change support yet).
void subdivide_model();
// The layer height map's width / height, for apply_uv_transform()'s non-square handling. 1 when
// there is no usable texture.
static float layer_texture_aspect(const TextureDisplacementLayer &layer);
// Returns a cached GPU thumbnail of layer's texture (decoding + uploading it the first time it
// is requested, or whenever its image_data changes), or nullptr if it has no usable texture.
// Panel-sized: box-filtered down to THUMBNAIL_MAX_PX, which is right for a list row and wrong for
// anything the shader samples - see get_layer_height_texture().
GLTexture *get_layer_thumbnail(const TextureDisplacementLayer &layer);
// The same texture at full resolution, for the fast-preview shader. One slot, shared by whichever
// layer is active, because that is the only one the bump shader ever shades.
GLTexture *get_layer_height_texture(const TextureDisplacementLayer &layer);
// The layer's colour texture for the fast preview's per-fragment quantization. Null when the
// layer is not colouring or its texture is grayscale.
GLTexture *get_layer_color_texture(const TextureDisplacementLayer &layer);
// A texture from the picker's library (see slic3r/GUI/TextureLibrary.hpp), read and uploaded
// once and then kept for the gizmo's lifetime. The decoded bytes are held alongside the GPU
// thumbnail so that picking the texture can hand the layer this very same image_data buffer -
// which both avoids re-reading the file and lets decode_height_texture()'s own cache (keyed by
// exactly this pointer) hit immediately on the first bake/preview.
struct LibraryTexture
{
std::shared_ptr<std::vector<unsigned char>> image_data;
std::unique_ptr<GLTexture> thumbnail;
};
const LibraryTexture *get_library_texture(const std::string &path);
// The layer's texture chooser: a drop-down whose closed state and every one of whose entries
// shows a large preview image on the left and the texture's name on the right, plus an adjacent
// button that imports an image file from disk into the user texture folder. Shipped and
// user-imported textures are listed under separate headings.
void render_texture_picker(TextureDisplacementLayer &layer);
void set_layer_texture(TextureDisplacementLayer &layer, const TextureLibraryEntry &entry);
void import_custom_texture(TextureDisplacementLayer &layer);
// Draws a picker row (image left, name right) on top of a full-width Selectable, and leaves the
// cursor below it. Shared by the drop-down's closed state and its individual entries so the two
// cannot drift apart. Returns true when the row is clicked.
bool texture_row(const char *id, const std::string &name, GLTexture *thumbnail, bool selected, float width);
float texture_row_height() const;
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active
// layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
// (a "pan" - drag anywhere on it for free 2D movement), plus two arrows along the patch's
// own U/V axes that constrain the drag to just that one axis for precise nudging. Anchored to
// the centroid/average-normal of the active layer's current paint patch (see
// libslic3r::compute_layer_paint_anchor()), so nothing is drawn if it has nothing painted yet.
//
// NOTE: the drag direction/sign below is this session's best-effort reasoning about which way
// the texture should appear to move as the handle is dragged - it could not be visually
// confirmed while writing it (no way to render/see pixels in this environment), so it may
// need a one-line sign flip once actually tested.
bool update_adjust_anchor(); // recomputes m_adjust_anchor_pos/normal; false if nothing painted
bool on_mouse_adjust_texture(const wxMouseEvent &mouse_event);
void render_adjust_texture_gizmo();
// Draws a small '+'/'-' next to the mouse over the 3D view while painting/selecting, so it is
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint();
// Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
// dominant-axis convention so dragging on-canvas maps consistently onto offset.
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
// Deliberately *fixed* - independent of the layer's offset - so that moving the handle cannot
// move the plane the handle's own motion is derived from, which would be a feedback loop.
Vec3f adjust_plane_point() const;
// Where the handle is actually drawn, in mesh-local coordinates. This is NOT just the patch's
// centroid: the handle *represents the texture's placement*, so it has to travel as `offset`
// changes. Pinning it to the centroid is why dragging it looked broken - the texture slid but
// the handle stayed put. Undoing apply_uv_transform()'s scale and rotation turns the layer's
// offset back into a displacement in mm within the patch's tangent plane, which is what gets
// added to the anchor here. That is exactly consistent with the drag arithmetic in
// on_mouse_adjust_texture(): the handle then tracks the cursor 1:1, and sits back on the anchor
// precisely when offset is zero.
Vec3f adjust_handle_center(const TextureDisplacementLayer &layer) const;
// The layer painted by the active slot, or nullptr if that slot has no layer yet.
TextureDisplacementLayer *active_layer();
const TextureDisplacementLayer *active_layer() const;
// Recomputes m_preview_glmodel from the volume's current (unbaked) paint state, using the same
// build_texture_displacement() algorithm as Bake. Called whenever the paint mask changes
// (stroke end, layer switch, undo/redo reload, post-bake refresh) rather than every frame -
// this is real mesh work (PNG sampling, vertex welding), not something to redo per paint stroke
// drag sample or idle repaint. With several painted layers this can be slow, so the actual
// computation runs in a background TextureDisplacementPreviewJob; this function only queues
// it and returns immediately, and m_preview_glmodel is updated later when it completes.
void rebuild_preview();
void render_preview_mesh();
// Alternate, GPU-only preview: perturbs shading normals from the active layer's height texture
// (a classic bump map) instead of actually moving vertices, using the
// resources/shaders/*/texture_displacement_bump.* shader. Faster than the true-displacement
// preview (no CPU meshing at all - just a per-vertex paint-weight buffer built at the same
// cadence as rebuild_preview()) but only shows the *active* layer, and any bump is a shading
// illusion, not real geometry - "Bake" always produces the true, exact result either way.
void rebuild_bump_preview_mesh();
void render_bump_preview_mesh();
// Feeds the active layer's painted patch + LSCM unwrap (if it's using that projection method)
// into Plater's docked UV-editor pane and shows it, or hides the pane if the active layer
// isn't using LSCM (or nothing is painted). Called whenever something that could change what
// the pane should show happens: paint changes, layer switch, projection method change, bake,
// and on shutdown (to hide it).
void update_uv_editor();
// Applies one island edit reported by the UV editor's drag/rotate gestures to the active layer.
// Deltas are incremental (see UVEditorCanvas::IslandEditFn); `finished` ends the gesture, which
// is when - and only when - the 3D preview is rebuilt, since doing that per mouse-move would
// queue a mesh recompute for every pixel of a drag.
void on_island_edited(int island, const Vec2f &offset_delta, float rotation_delta, float scale_factor, bool finished);
// Applies a committed vertex/edge edit from the UV editor's Vertex/Edge modes: each entry is an
// unwrapped-vertex index and its new raw-unwrap coordinate. Maps the unwrapped index to a mesh
// vertex and stores a per-vertex UV override on the layer (see lscm_uv_overrides), then rebuilds the
// preview so the baked geometry follows.
void on_uv_vertex_edited(const std::vector<std::pair<int, Vec2f>> &edits);
// UV-editor sub-element select mode, mirrored into the canvas: 0 = Island, 1 = Vertex, 2 = Edge.
int m_uv_select_mode = 0;
// One affine per island (columns: x basis, y basis, translation), mapping the unwrap's raw mm
// coordinates to texture UVs - the same type as UVEditorCanvas::IslandTransform, spelled out
// here so this header needn't drag in wxGLCanvas/glad. Cheap to recompute (it is per *island*,
// not per vertex), which is what lets an island drag update the pane without re-uploading a
// single vertex.
std::vector<Eigen::Matrix<float, 2, 3>> uv_editor_island_transforms(const TextureDisplacementLayer &layer);
// Handles a toolbar command forwarded from the UV pane that needs the layer data the canvas
// doesn't hold (average island scale, cut island). Takes the command as an int (a cast of
// UVEditorCanvas::Command) so this header needn't pull in glad/wxGLCanvas via the canvas header.
void on_uv_command(int cmd);
// Splits one unwrap chart in two by marking the mesh edges that straddle the plane through its
// 3D centroid, perpendicular to its longest axis, as seams (#17). The re-unwrap then separates it.
void cut_island(TextureDisplacementLayer &layer, int chart);
// Captures the current camera's right/up axes into the layer's projector (#6), transformed into
// the volume's local space so the projection is stable as the object is later moved/rotated.
void capture_view_projection(TextureDisplacementLayer &layer);
// Manual seam marking (#9): a mode where clicking the model toggles the nearest mesh edge in the
// active layer's lscm_seam_edges, so the unwrap can be cut exactly where the user wants - the
// Blender "mark seam" workflow. Painting is suppressed while it is on.
bool m_seam_edit_mode = false;
GLModel m_seam_glmodel; // the current seam edges, highlighted on the mesh
bool on_mouse_seam(const wxMouseEvent &mouse_event);
void toggle_seam_at(const Vec2d &mouse_pos);
void rebuild_seam_overlay();
void render_seam_overlay();
// The mesh edge nearest the mouse, in the volume's own vertex indices, or {-1,-1} if the ray misses.
// Factored out of toggle_seam_at() so the same pick can drive a live hover highlight (below) that
// shows which edge a click would toggle - the "I don't know how it works" feedback the user hit.
std::pair<int, int> seam_edge_at(const Vec2d &mouse_pos) const;
std::pair<int, int> m_seam_hover_edge{ -1, -1 };
// The vertex a click would pick in shortest-path mode, so the target is visible on hover the same
// way the edge is in normal mode. -1 when nothing is under the cursor (or not in path mode).
int m_seam_hover_vertex = -1;
GLModel m_seam_hover_glmodel;
void rebuild_seam_hover_overlay();
// Shortest-path seam marking, for dense meshes where clicking every single triangle edge is
// tedious: in this sub-mode a click picks the nearest vertex, and the next click marks every edge
// on the shortest surface path between the two as a seam - so a whole seam line is drawn with two
// clicks. The end vertex becomes the next start, so a multi-segment seam chains click by click.
bool m_seam_path_mode = false;
int m_seam_path_anchor = -1; // mesh vertex the path starts from, or -1
GLModel m_seam_anchor_glmodel; // the anchor's incident edges, highlighted
int seam_vertex_at(const Vec2d &mouse_pos) const; // nearest mesh vertex under the cursor
void mark_seam_path(int v_from, int v_to); // seam every edge on the shortest path
void rebuild_seam_anchor_overlay();
// Set while an island gesture is in flight, so the undo snapshot is taken once at the start of
// the drag (capturing the state *before* it) rather than on every motion event.
bool m_island_drag_active = false;
// Which of the up to TEXTURE_DISPLACEMENT_MAX_LAYERS paint masks the brush currently writes
// into. Always a valid slot index (0 by default) so the base class's per-volume selector
// machinery always has something to work with, even before any texture has been added -
// painting into a slot with no texture assigned is harmless, it just has no visible/bake
// effect until a texture is added to that slot.
int m_active_layer_slot = 0;
bool m_bake_in_progress = false;
// When set, the true-displacement geometry is rebuilt on every parameter change (live), instead of
// only once the slider being dragged is released. On by default so painting/added textures show
// straight away without needing to nudge a slider first.
bool m_auto_update = true;
// Subdivision is now count-based (split the whole mesh 1..5 times) rather than a target edge
// length, and is previewed as a wireframe before it is committed: nothing is written to the model
// until "Apply". While previewing, the would-be subdivided mesh is drawn as a wireframe overlay so
// the added density is visible; "Done" ends the preview without touching the model. The normal
// "Show mesh wireframe" toggle is left alone, so a wireframe the user already had on stays on.
// 0 is a real value meaning "no subdivision": it previews nothing and Apply is a no-op. Apply
// snaps the slider back to it, because each pass quadruples the triangle count - leaving the
// count where it was would immediately re-preview N more passes on top of the mesh that was just
// committed, i.e. the most expensive thing the panel can do, on every Apply.
int m_subdivide_count = 1;
bool m_subdivide_editing = false;
int m_subdivide_preview_tris = -1; // triangle count of the previewed result, shown in the panel
GLModel m_subdivide_preview_glmodel;
void rebuild_subdivide_preview();
void render_subdivide_preview();
// Adaptive subdivision: refine only the painted area, down to a target edge length, via
// conformal longest-edge bisection (subdivide_mesh_adaptive()). Unlike the count-based uniform
// path it does not touch the unpainted rest of the model, and - because it is driven by the paint
// - it can carry that paint forward across the topology change (children of a painted triangle
// are painted), so the region survives the subdivision instead of being dropped.
bool m_subdivide_adaptive = false;
float m_subdivide_target_mm = 0.f; // 0 = not yet seeded; filled from the mesh on first show
// Feature-adaptive sub-mode: put the triangles where the *displaced surface* bends (texture
// curvature) rather than spreading them evenly. `detail_mm` is the chord-error tolerance ("Detail"
// slider: how far the true surface may sit off the flat triangle before it is split); the target
// above stays in play as a coarse baseline ("Max edge"), and `min_edge_mm` is the hard floor
// ("Min edge"). See subdivide_mesh_adaptive().
bool m_subdivide_feature = false;
float m_subdivide_detail_mm = 0.05f;
float m_subdivide_min_edge_mm = 0.1f;
// Edge length the band straddling the paint's boundary is refined to (0 = leave it alone). Applies
// in both adaptive sub-modes, because it is not a texture-detail criterion: the bake steps the
// surface from full displacement to zero across that boundary whatever the texture is doing, and
// the chord-error test cannot see that step at all - its sampler has no per-point paint test, so
// just outside the paint it goes on reporting the same smooth height field. Without this the
// transition keeps the input's density and the rim of an unpainted island comes out as a ring of
// large, steeply tilted triangles. See collect_paint_region() and subdivide_mesh_adaptive().
float m_subdivide_border_mm = 0.4f;
// Edge length triangles straddling a *colour* boundary are refined to (0 = ignore colour). Its own
// control rather than a share of "Detail (mm)" because the two measure different things: Detail is
// a chord error in mm of surface deviation, this is a triangle size in mm along a step the chord
// test cannot see at all - the height field is perfectly smooth across a change of filament, so
// without this a colour boundary lands on whatever triangles the relief happened to need, which on
// a flat surface is none. Only ever costs anything where a boundary actually runs.
float m_subdivide_color_mm = 0.3f;
// How many thousand triangles refinement may *add* (the mesh's own count is added on before it is
// passed as subdivide_mesh_adaptive()'s absolute cap, so the control still means something on a
// dense model). Refinement is worst-error-first, so hitting the budget still yields the best mesh
// that many triangles can buy - and it is what keeps a fine "Detail" over a noisy texture from
// turning into an out-of-memory, or an unrenderable preview wireframe.
//
// The default used to be 1500 (i.e. +1.5 M triangles), which is what made Standard mode's Bake
// take minutes: every stage after the subdivision - the displacement itself, the convex hull, the
// GLModel upload, and the re-slice changed_object() triggers - then runs on a mesh two orders of
// magnitude denser than the input. 750k is still far finer than any FDM nozzle resolves at the
// 0.02 mm detail tolerance Standard uses, and the slider goes to 2000 for anyone who wants more.
int m_subdivide_budget_k = 750;
void subdivide_model_adaptive();
// Fills `region` (per current-mesh triangle, a REFINE_* bitmask) from the union of every layer's
// painted area plus the band straddling its edge. If `paint` is non-null, also fills the per-layer
// coverage map the subdivision carries forward - the expensive half, skipped by the live preview,
// which only needs the region. Returns false when nothing is painted at all.
static bool collect_paint_region(const TriangleMesh &mesh, const TextureDisplacementFacetsData &facets,
std::vector<uint8_t> &region,
std::array<LayerPaintMap, TEXTURE_DISPLACEMENT_MAX_LAYERS> *paint);
// Runs the volume's TextureDisplacementOptions smoothing over the *already committed* geometry,
// restricted to the painted area. The same settings are folded into Preview/Bake automatically;
// this is the escape hatch for relief that has already been baked in, where there is no
// displacement pass left to attach them to. Topology-preserving, so unlike subdivide and remesh it
// keeps every paint channel - including texture displacement - exactly as it was.
void smooth_model();
// Isotropic remeshing (CGAL) to even out wildly varying triangle sizes so displacement has a
// consistent density to work with. Target edge length in mm; 0 means "not yet initialised", filled
// with the mesh's mean edge length the first time the control is shown. Like subdivide, it replaces
// the geometry, but unlike subdivide it keeps every paint channel: prepare_mesh() carries the
// texture-displacement masks across spatially, which is also what lets Standard mode remesh *after*
// the user has painted.
float m_remesh_target_edge_mm = 0.f;
// Dihedral angle above which an edge counts as a hard feature and is held fixed by the remesher.
// Off by default would round every sharp edge off, so this is on; 0 disables the protection.
float m_remesh_sharp_angle_deg = 40.f;
bool m_remesh_keep_sharp_edges = true;
void remesh_model();
// Live, pre-bake preview of the true displaced geometry (built by the same algorithm Bake
// uses). Empty/uninitialized whenever nothing is painted yet, in which case the gizmo falls
// back to the standard paint-mask overlay like every other painting gizmo.
GLModel m_preview_glmodel;
// Set while a layer parameter slider has changed since the last rebuild_preview() call but the
// mouse button driving the drag hasn't been released yet - see on_render_input_window().
bool m_preview_params_dirty = false;
// See rebuild_bump_preview_mesh()/render_bump_preview_mesh(). On by default: it is the cheap,
// instant-updating preview, so it is the better first impression while painting. The true-
// displacement view (a background CPU remesh) is one click away in the View row when the user
// wants an exact look at what Bake will produce.
bool m_use_bump_preview = true;
// Set from the UV editor's per-move island edits instead of rebuilding the (potentially large) bump
// mesh synchronously inside that mouse handler - doing the rebuild there stalled both the UV pane
// and the 3D view. The rebuild is instead coalesced to once per 3D frame (render_painter_gizmo).
bool m_bump_preview_dirty = false;
GLModel m_bump_preview_glmodel;
// Translucent tint over the active layer's painted triangles, drawn on top of whichever preview
// is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used
// in either preview mode - it is coincident with the surface and simply covers it - so the only
// paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all
// until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
// stroke.
GLModel m_paint_overlay_glmodel;
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
// separate from m_bump_preview_dirty so a stroke refreshes only the small painted patch per frame,
bool m_paint_overlay_dirty = false;
void rebuild_paint_overlay();
void render_paint_overlay();
// Whether render_bump_preview_mesh() would actually draw something. Checked before the real volume
// is hidden: with no layer, no texture or no shader the bump path draws nothing, and hiding the
// volume for it left the model invisible.
bool bump_preview_ready() const;
// Whether the current bump mesh carries a precomputed per-vertex uv (LSCM) that the shader
// should sample at directly, rather than projecting in-shader. Set by rebuild_bump_preview_mesh().
bool m_bump_preview_uses_vertex_uv = false;
// The palette the fast preview's per-triangle filament indices were built against, captured when
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked
// into the mesh can never be resolved against a different set of filaments than they were computed
// from - loading a filament mid-session would otherwise recolour a stale preview at random.
std::vector<PaletteEntry> m_bump_preview_palette;
// GPU island drag: while an island is dragged in the UV editor, the bump mesh is baked once (with
// the dragged island's vertices flagged, v_normal.y = 1) and then moved purely through the shader's
// island_delta uniform - one uniform update per mouse move, no rebuild - so it tracks the cursor
// as smoothly as Adjust placement. m_bump_active_chart is the dragged island (or -1);
// m_bump_active_vertex flags its base vertices; m_bump_baked_active_xf is that island's placement
// baked into the current mesh, against which the live delta is measured; m_bump_island_delta is the
// resulting final-uv-space affine handed to the shader (identity except mid-drag).
int m_bump_active_chart = -1;
std::vector<uint8_t> m_bump_active_vertex;
Eigen::Matrix<float, 2, 3> m_bump_baked_active_xf = Eigen::Matrix<float, 2, 3>::Identity();
Eigen::Matrix<float, 2, 3> m_bump_island_delta = Eigen::Matrix<float, 2, 3>::Identity();
void compute_bump_active_vertices(const std::vector<int> &charts);
// The set of islands the current UV-editor drag moves together: the pane's multi-selection unioned
// with each selected island's join group (see build_island_move_set()). Populated at drag start and
// cleared when it finishes. A move applies the same offset to every island in it; rotate/scale act
// only on the primary. Empty when no move drag is in flight.
std::vector<int> m_island_move_set;
// All islands that must move with `primary`: the pane's multi-selection plus, for each of those, the
// charts sharing its join group in `layer`. Always contains `primary`.
std::vector<int> build_island_move_set(const TextureDisplacementLayer &layer, int primary) const;
// The join-group id of chart `c`: its explicit entry in `groups`, or `c` itself (its own singleton)
// when unset. Two charts move together iff this matches.
static int island_group_of(const std::vector<int> &groups, int c);
// Merges chart `b`'s join group into chart `a`'s (materialising `groups` to `chart_count` first).
static void join_island_groups(std::vector<int> &groups, int a, int b, int chart_count);
// Final per-vertex texture uv for the projections the shader can't reconstruct itself - LSCM (an
// unwrap) and ViewProjected (a projector plane the shader doesn't know). One entry per patch/base
// vertex, already through apply_uv_transform(). Empty for Triplanar/Cylindrical/Spherical, which
// the shader projects on its own. Shared by the bump preview and the UV-check overlay.
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer) const;
// `patch` with its vertices moved into world millimetres - the space the bake maps the texture in
// (see build_texture_displacement()). Returned by value because the caller usually still needs the
// original: the patch doubles as render geometry, which is drawn through the volume's own matrix.
indexed_triangle_set patch_in_world(const indexed_triangle_set &patch) const;
// UV-check overlay drawn over the painted patch to sanity-check the unwrap (#13/#14). Built by
// rebuild_uvcheck_mesh(), drawn by render_uvcheck_mesh() with the "texture_displacement_uvcheck"
// shader. Checker works for any projection; Distortion needs the per-vertex LSCM uv.
enum class UVCheckMode { None, Checker, Distortion };
UVCheckMode m_uv_check_mode = UVCheckMode::None;
GLModel m_uvcheck_glmodel;
bool m_uvcheck_uses_vertex_uv = false;
void rebuild_uvcheck_mesh();
void render_uvcheck_mesh();
// The UV editor pane is opened only on the user's explicit request (this toggle in the panel),
// never automatically just because a patch exists - auto-popping it whenever there was "a
// selection to process" is exactly what the user asked to stop. update_uv_editor() keeps the pane
// hidden unless this is set. Reset on gizmo shutdown so reopening the gizmo doesn't reopen the pane.
bool m_show_uv_editor = false;
// The unwrap is expensive, so it is recomputed only when the user explicitly asks for it (the
// "Unwrap" button), not on every paint stroke or slider nudge. This is set by that button and
// consumed by the next update_uv_editor() call, which is the only path that re-solves the unwrap;
// every other call merely refreshes the cheap per-island affine transforms over the existing one.
bool m_uv_unwrap_pending = false;
// Set alongside m_uv_unwrap_pending only by the Unwrap button, so the connected-net auto-layout runs
// on a genuine re-unwrap but not on a refresh re-solve (a vertex-edit commit or undo), which must
// leave island placements untouched.
bool m_uv_apply_connected_net = false;
// Signature of the per-vertex UV overrides last reflected in the pane. When it changes without the
// user pressing Unwrap - a vertex/edge edit committing, or an undo/redo reverting one - the pane
// is re-solved so its geometry follows, even though a plain edit otherwise never re-solves (#Feat2).
size_t m_uv_overrides_sig = 0;
// What the UV pane's background currently holds, so update_uv_editor() only re-uploads it when the
// choice actually changes (the height texture is large; re-sending it every stroke would be waste).
enum class UVBackground { None, Height, Checker };
UVBackground m_uv_editor_bg = UVBackground::None;
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
// while the Distortion check mode is on. Empty otherwise.
std::vector<ColorRGBA> m_uv_editor_distortion_colors;
void compute_uv_editor_distortion_colors(const indexed_triangle_set &patch);
// Plain triangle-edge overlay on the mesh (#8), toggled independently of the check modes.
bool m_wireframe_overlay = false;
GLModel m_wireframe_overlay_glmodel;
size_t m_wireframe_overlay_vcount = 0; // topology signature, so it rebuilds only on a real change
void rebuild_wireframe_overlay(); // from the base mesh (bump/paint mode)
void build_wireframe_from_its(const indexed_triangle_set &its); // from an explicit mesh, no early-out
void refresh_wireframe(); // pick base vs displaced source for the current view
void render_wireframe_overlay();
// The displaced preview geometry the last preview job produced, kept so the wireframe overlay can be
// drawn on the raised surface actually shown in the true-displacement view (#: "wireframe in real mode").
indexed_triangle_set m_preview_its;
// Bumped on every rebuild_preview() call; a background TextureDisplacementPreviewJob's result
// is only applied if this hasn't moved on since the job was queued (see rebuild_preview()),
// so a burst of edits can't have an earlier, now-stale job clobber a later one's result.
//
// Shared with the worker thread (hence the atomic) so a running job can notice mid-computation
// that it has been superseded and abort, instead of running to completion for a result that will
// only be discarded on arrival.
std::shared_ptr<std::atomic<uint64_t>> m_preview_generation = std::make_shared<std::atomic<uint64_t>>(0);
// At most one preview job is ever queued. The UI job worker is a single FIFO queue shared with
// Bake (and with arrange/orient/send), and rebuild_preview() is called on every stroke end, every
// slider release and - with "Auto update" on - every frame of a slider drag. Queuing one full
// displacement per call built a backlog that took minutes to drain: the preview appeared frozen,
// and a Bake pressed afterwards sat behind the whole queue. So a request made while a job is in
// flight is recorded here and issued once that job settles, collapsing any number of edits into a
// single follow-up run.
bool m_preview_job_running = false;
bool m_preview_job_pending = false;
void queue_preview_job();
// Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
// was current the last time each thumbnail was built (see get_layer_thumbnail()).
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
// Full-resolution height texture for the bump shader, keyed the same way (see
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
// blur the bake will apply.
std::unique_ptr<GLTexture> m_height_tex;
const void *m_height_tex_source = nullptr;
float m_height_tex_smoothing = -1.f;
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
std::unique_ptr<GLTexture> m_color_tex;
const void *m_color_tex_source = nullptr;
float m_color_tex_smoothing = -1.f;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures;
// Everything the *unwrap* depends on. update_uv_editor() runs from rebuild_preview(), i.e. on
// every stroke end and every slider release - but depth/tiling/rotation/offset/blend change
// none of this, so re-extracting the patch and re-solving on those edits would be pure waste.
// Held as the real values rather than a hash: TriangleSplittingData has an exact operator==, so
// there is no reason to accept a hash's (however unlikely) chance of showing a stale unwrap.
struct UVEditorState
{
int slot = -1;
const void *image_data = nullptr;
float seam_angle = -1.f;
float padding = -2.f;
TriangleSelector::TriangleSplittingData facets;
// Manual/auto seam edges also change the unwrap, so a change here must force a re-solve just
// like the facets do (marking a seam leaves the paint mask untouched).
std::vector<std::pair<int, int>> seam_edges;
bool operator==(const UVEditorState &other) const
{
return slot == other.slot && image_data == other.image_data && seam_angle == other.seam_angle &&
padding == other.padding && facets == other.facets && seam_edges == other.seam_edges;
}
};
UVEditorState m_uv_editor_state;
// Bounds of the UVs last handed to the pane, purely so the panel can show where the unwrap
// actually landed - it is packed in mm and then divided by the tile size, so it is easy for it
// to end up far outside the texture's first tile without any of that being visible.
Vec2f m_uv_editor_bbox_min = Vec2f::Zero();
Vec2f m_uv_editor_bbox_max = Vec2f::Zero();
// The unwrap m_uv_editor_state produced, kept so that changing tiling/rotation/offset only costs
// re-running apply_uv_transform() over it, not another extraction and solve.
PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to
// grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
// open/close within a session, so the choice sticks while working.
bool m_undocked = false;
// Smooth scrolling for the layer-stack child region. ImGui jumps a fixed number of lines per
// wheel notch, which on a list of tall per-layer blocks reads as a hard jolt rather than a
// scroll. The wheel is intercepted (ImGuiWindowFlags_NoScrollWithMouse) and moves a *target*
// offset instead; the real scroll is eased toward it over the following frames.
float m_layer_scroll_target = 0.f;
float m_layer_scroll_applied = -1.f; // what the easing wrote last frame; <0 until the first one
// See the "Adjust Texture" block of private methods above.
bool m_adjust_texture_mode = false;
bool m_adjust_anchor_valid = false;
Vec3f m_adjust_anchor_pos = Vec3f::Zero(); // mesh-local
Vec3f m_adjust_anchor_normal = Vec3f::UnitZ(); // mesh-local
// Pan: free drag anywhere on the flat panel, moves offset along both axes. AxisU/AxisV: drag
// the corresponding arrow, moves offset along only that one axis.
enum class AdjustHandle { None, Pan, AxisU, AxisV };
AdjustHandle m_adjust_drag_handle = AdjustHandle::None;
Vec2f m_adjust_drag_start_offset = Vec2f::Zero();
// Anchor-relative planar position (see project_planar()) of the point under the mouse at the
// moment the current drag started; every subsequent frame's delta is measured against this,
// rather than accumulated frame-to-frame, to avoid drift.
Vec2f m_adjust_drag_start_planar = Vec2f::Zero();
// Lazily-built unit quad (the pan panel) and unit line-with-arrowhead (reused, rotated, for
// both the U and V axis arrows), transformed into place at render time.
GLModel m_adjust_panel_glmodel;
GLModel m_adjust_arrow_glmodel;
std::map<std::string, wxString> m_desc;
// The tool's SVG (toolbar_texture_displacement.svg) uploaded once as a GL texture, so it can be
// used as an ImGui image button in the panel (currently the "add layer" affordance next to the
// Texture layers heading). Lazily loaded on first use, when a GL context is guaranteed current.
GLTexture m_tool_icon;
bool m_tool_icon_tried = false;
unsigned int tool_icon_id(); // 0 if the icon could not be loaded
// Icons for the panel's selection-mode and view-mode button rows. Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
// the theme's normal (grey) foreground colour and an active one shows it in its original colours -
// matching the toolbar's selected/unselected look. Uploaded once on first panel render.
IconManager m_panel_icons;
std::map<std::string, IconManager::Icons> m_panel_icon_map; // file name -> [normal, colour, disabled]
bool m_panel_icons_tried = false;
void ensure_panel_icons();
};
} // namespace Slic3r::GUI
#endif // slic3r_GLGizmoTextureDisplacement_hpp_

View File

@@ -22,6 +22,7 @@
//#include "slic3r/GUI/Gizmos/GLGizmoHollow.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSeam.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoMmuSegmentation.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSimplify.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoEmboss.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoSVG.hpp"
@@ -164,6 +165,13 @@ void GLGizmosManager::switch_gizmos_icon_filename()
case(EType::FuzzySkin):
gizmo->set_icon_filename(m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg");
break;
case(EType::TextureDisplacement):
// One shared icon in both themes (no dedicated dark variant yet) - but it must still be
// *this* gizmo's icon. Handing it the fuzzy-skin one here quietly replaced the icon set at
// construction, so the toolbar ended up showing two identical fuzzy-skin buttons after any
// light/dark switch.
gizmo->set_icon_filename("toolbar_texture_displacement.svg");
break;
case(EType::MeshBoolean):
gizmo->set_icon_filename(m_is_dark ? "toolbar_meshboolean_dark.svg" : "toolbar_meshboolean.svg");
break;
@@ -213,6 +221,8 @@ bool GLGizmosManager::init()
m_gizmos.emplace_back(new GLGizmoSeam(m_parent, m_is_dark ? "toolbar_seam_dark.svg" : "toolbar_seam.svg", EType::Seam));
m_gizmos.emplace_back(new GLGizmoFuzzySkin(m_parent, m_is_dark ? "toolbar_fuzzy_skin_paint_dark.svg" : "toolbar_fuzzy_skin_paint.svg", EType::FuzzySkin));
m_gizmos.emplace_back(new GLGizmoMmuSegmentation(m_parent, m_is_dark ? "mmu_segmentation_dark.svg" : "mmu_segmentation.svg", EType::MmSegmentation));
// One shared icon (no dedicated dark variant yet); it recolours acceptably in both themes.
m_gizmos.emplace_back(new GLGizmoTextureDisplacement(m_parent, "toolbar_texture_displacement.svg", EType::TextureDisplacement));
m_gizmos.emplace_back(new GLGizmoEmboss(m_parent, m_is_dark ? "toolbar_text_dark.svg" : "toolbar_text.svg", EType::Emboss));
m_gizmos.emplace_back(new GLGizmoSVG(m_parent));
m_gizmos.emplace_back(new GLGizmoMeasure(m_parent, m_is_dark ? "toolbar_measure_dark.svg" : "toolbar_measure.svg", EType::Measure));
@@ -524,6 +534,8 @@ bool GLGizmosManager::gizmo_event(SLAGizmoEventType action, const Vec2d& mouse_p
return dynamic_cast<GLGizmoCut3D*>(m_gizmos[Cut].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == FuzzySkin)
return dynamic_cast<GLGizmoFuzzySkin*>(m_gizmos[FuzzySkin].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == TextureDisplacement)
return dynamic_cast<GLGizmoTextureDisplacement*>(m_gizmos[TextureDisplacement].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == MeshBoolean)
return dynamic_cast<GLGizmoMeshBoolean*>(m_gizmos[MeshBoolean].get())->gizmo_event(action, mouse_position, shift_down, alt_down, control_down);
else if (m_current == BrimEars)
@@ -537,6 +549,7 @@ bool GLGizmosManager::is_paint_gizmo()
return m_current == EType::FdmSupports ||
m_current == EType::MmSegmentation ||
m_current == EType::FuzzySkin ||
m_current == EType::TextureDisplacement ||
m_current == EType::Seam;
}
@@ -1514,6 +1527,8 @@ std::string get_name_from_gizmo_etype(GLGizmosManager::EType type)
return "Color Painting";
case GLGizmosManager::EType::FuzzySkin:
return "Fuzzy Skin Painting";
case GLGizmosManager::EType::TextureDisplacement:
return "Texture Displacement";
default:
return "";
}

View File

@@ -84,6 +84,7 @@ public:
Seam,
FuzzySkin,
MmSegmentation,
TextureDisplacement,
Emboss,
Svg,
Measure,

View File

@@ -176,6 +176,7 @@ private:
// Use those values to disable selection of active extruders
bool m_is_dark = false;
bool is_osx{false};
int m_min_value;
int m_max_value;
int m_lower_value;
@@ -200,6 +201,10 @@ private:
void *m_one_layer_on_hover_id;
void *m_one_layer_off_id;
void *m_one_layer_off_hover_id;
void* m_one_layer_on_light_id;
void* m_one_layer_on_hover_light_id;
void* m_one_layer_off_light_id;
void* m_one_layer_off_hover_light_id;
void* m_one_layer_on_dark_id;
void* m_one_layer_on_hover_dark_id;
void* m_one_layer_off_dark_id;

View File

@@ -2586,7 +2586,11 @@ void ImGuiWrapper::push_toolbar_style(const float scale)
ImGui::PushStyleColor(ImGuiCol_FrameBgActive, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 1.00f)); // 10
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(238 / 255.0f, 238 / 255.0f, 238 / 255.0f, 0.00f)); // 11
ImGui::PushStyleColor(ImGuiCol_TextSelectedBg, COL_GREEN_LIGHT); // 12
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(1.00f, 1.00f, 1.00f, 1.00f));//13
// The checkbox/radio frame behind this is drawn fully transparent (see FrameBg above,
// alpha 0), showing the light window background through it - a white check mark there is
// invisible. Dark mode doesn't have this problem (its window background is dark), so only
// this branch needs a check mark color with real contrast against a light background.
ImGui::PushStyleColor(ImGuiCol_CheckMark, ImVec4(0.f, 156 / 255.f, 136 / 255.f, 1.00f));//13
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrab, ImVec4(0.42f, 0.42f, 0.42f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabHovered, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));
ImGui::PushStyleColor(ImGuiCol_ScrollbarGrabActive, ImVec4(0.93f, 0.93f, 0.93f, 1.00f));

View File

@@ -0,0 +1,164 @@
#include "TextureDisplacementBakeJob.hpp"
#include <algorithm>
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/GLCanvas3D.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
namespace Slic3r::GUI {
TextureDisplacementBakeJob::TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function<void()> on_finished)
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementBakeJob::process(Ctl &ctl)
{
const std::string status = _u8L("Baking texture displacement");
ctl.update_status(1, status);
// Only ever touches m_input (captured by value before this job was queued) and local state -
// never the live Model - so this is safe to run concurrently with the UI thread.
//
// The progress hook matters for more than cosmetics: the framework's progress notification only
// grows a close button once it reaches 100%, so a job that reports 0 and nothing else leaves an
// uncloseable notification pinned on screen. It also carries the Cancel button's effect into the
// bake, which on a subdivided mesh can run for several seconds.
// Colour, when any layer asks for it, is computed in the same pass as the displacement: both need
// the same per-layer projection and UV work, and doing it twice would double the expensive part.
TextureColorRequest color_request;
TextureColorRequest *color = nullptr;
if (!m_input.color.empty()) {
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
m_input.color.dither_cell_mm);
color_request.despeckle_passes = m_input.color.despeckle_passes;
color_request.out_triangle = &m_triangle_color;
if (color_request.quantize)
color = &color_request;
}
int last_reported = 1;
m_result = TriangleMesh(build_texture_displacement(
m_input.base_mesh, m_input.layers, m_input.facets_data, m_input.options,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
// The notification repaints (and wakes the idle loop) on every call, so only push a
// message when the displayed integer percentage actually moves.
if (percent > last_reported) {
last_reported = percent;
ctl.update_status(percent, status);
}
return true;
},
color, m_input.volume_to_world));
// Always finish at 100: this is what closes the notification. Reported even on cancel, where
// build_texture_displacement() returns an empty mesh and finalize() commits nothing.
ctl.update_status(100, status);
}
void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &eptr)
{
struct OnExit
{
std::function<void()> fn;
~OnExit() { if (fn) fn(); }
} on_exit{m_on_finished};
if (canceled || eptr || m_result.empty())
return;
Plater *plater = wxGetApp().plater();
const auto commit = [this, plater]() {
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
if (volume == nullptr)
return;
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
// Colour lands in mmu_segmentation_facets, merged *over* whatever is already painted there
// rather than replacing it: a triangle the texture does not colour keeps its existing filament,
// and one the user never painted at all stays at NONE, which already means "the volume's own
// filament". That is what confines the effect to the painted area without having to invent a
// colour for everything outside it. Safe to index straight onto the new mesh - the bake is
// topology-preserving, so triangle i is still triangle i.
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
TriangleSelector selector(volume->mesh());
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
if (!existing.bitstream.empty())
selector.deserialize(existing, false);
for (size_t i = 0; i < m_triangle_color.size(); ++i)
if (m_triangle_color[i] > 0)
selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i]));
volume->mmu_segmentation_facets.set(selector);
}
// Clear the paint mask of every layer that was actually baked so a repeat bake (or the paint
// overlay) doesn't act on triangles that no longer represent the same unbaked surface. The
// texture layer definitions themselves (and paint outside the baked area, if any) are left
// untouched so the user can keep sculpting with the same textures.
for (const TextureDisplacementLayer &layer : m_input.layers)
if (!layer.empty() && layer.slot >= 0 && layer.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
volume->texture_displacement_facet(layer.slot).reset();
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
auto it = std::find(objs.begin(), objs.end(), object);
if (it != objs.end())
obj_list->update_info_items(size_t(it - objs.begin()));
}
plater->changed_object(*object);
};
// Standard mode's Bake is remesh -> subdivide -> displace under a single snapshot, and this job runs
// long after that snapshot's scope has closed. Adding one here would put an undo step *between* the
// subdivision and the displacement: the first Undo would land on a mesh carrying every added
// triangle and no relief at all, and pressing Bake again from there would subdivide that mesh a
// second time. So the caller says who owns the undo step.
if (m_input.take_snapshot) {
Plater::TakeSnapshot snapshot(plater, _u8L("Bake texture displacement"), UndoRedo::SnapshotType::GizmoAction);
commit();
} else {
commit();
}
}
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
std::function<void()> on_finished, bool take_snapshot)
{
TextureDisplacementBakeInput input;
input.color = color;
input.take_snapshot = take_snapshot;
input.volume_id = volume.id();
input.base_mesh = volume.mesh().its;
input.layers = volume.texture_displacement_layers;
input.options = volume.texture_displacement_options;
input.volume_to_world = texture_displacement_volume_to_world(volume);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementBakeJob>(std::move(input), std::move(on_finished)));
}
} // namespace Slic3r::GUI

View File

@@ -0,0 +1,68 @@
#ifndef slic3r_TextureDisplacementBakeJob_hpp_
#define slic3r_TextureDisplacementBakeJob_hpp_
#include <functional>
#include <vector>
#include "libslic3r/Color.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "Job.hpp"
namespace Slic3r::GUI {
// Everything process() needs, captured by value on the main thread when the job is queued so the
// worker thread never touches the live Model concurrently with the UI (mirrors how EmbossJob's
// DataBase is captured before process() runs).
struct TextureDisplacementBakeInput
{
ObjectID volume_id;
indexed_triangle_set base_mesh;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementFacetsData facets_data;
TextureDisplacementOptions options;
// Mesh coordinates -> world millimetres. Captured here with everything else so the worker never
// reaches back into the live Model for it. See build_texture_displacement().
Transform3d volume_to_world = Transform3d::Identity();
// Captured on the main thread. Empty unless some layer is colouring, in which case the bake also
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
// paint gizmo writes - alongside the displaced geometry.
TextureColorSettings color;
// Whether this job pushes its own undo step when it commits. False when the caller has already
// taken one that is meant to cover the displacement as well - Standard mode's Bake, which remeshes
// and subdivides first and has to undo as a single action.
bool take_snapshot = true;
};
// Bakes a volume's painted texture-displacement layers into real mesh geometry in the background,
// then commits the result on the main thread - mirrors EmbossJob's UpdateJob/update_volume()
// bake-and-commit pattern (see EmbossJob.cpp).
class TextureDisplacementBakeJob : public Job
{
public:
TextureDisplacementBakeJob(TextureDisplacementBakeInput &&input, std::function<void()> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
private:
TextureDisplacementBakeInput m_input;
TriangleMesh m_result;
// Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value
// (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest.
std::vector<uint8_t> m_triangle_color;
std::function<void()> m_on_finished;
};
// Captures `volume`'s current mesh/layers/paint data and queues a TextureDisplacementBakeJob on
// the app's UI job worker. `on_finished` is always called once the job settles (success, failure,
// or cancellation), so the caller can clear its own "bake in progress" UI state. Must be called
// from the main thread.
void queue_texture_displacement_bake(const ModelVolume &volume, const TextureColorSettings &color,
std::function<void()> on_finished, bool take_snapshot = true);
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementBakeJob_hpp_

View File

@@ -0,0 +1,117 @@
#include "TextureDisplacementPrepareJob.hpp"
#include <algorithm>
#include <optional>
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/GUI_ObjectList.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Plater.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/Utils/UndoRedo.hpp"
namespace Slic3r::GUI {
TextureDisplacementPrepareJob::TextureDisplacementPrepareJob(
TextureDisplacementPrepareInput &&input, std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
: m_input(std::move(input)), m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementPrepareJob::process(Ctl &ctl)
{
const std::string status = _u8L("Preparing mesh for texture displacement");
ctl.update_status(1, status);
// Only ever touches m_input (captured by value before this job was queued) and local state - never
// the live Model - so this is safe to run concurrently with the UI thread.
//
// The progress hook is not just cosmetic: the framework's notification only grows a close button
// once it reaches 100%, and below that it shows Cancel, which is wired through here. Reporting is
// throttled to whole percentage points because every call repaints the notification and wakes the
// idle loop.
int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.params, m_input.color.palette,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
if (percent > last_reported) {
last_reported = percent;
ctl.update_status(percent, status);
}
return true;
});
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
}
void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &eptr)
{
TextureDisplacementPrepareOutcome outcome = TextureDisplacementPrepareOutcome::Failed;
struct OnExit
{
std::function<void(TextureDisplacementPrepareOutcome)> fn;
const TextureDisplacementPrepareOutcome *outcome;
~OnExit() { if (fn) fn(*outcome); }
} on_exit{m_on_finished, &outcome};
if (canceled || eptr)
return;
if (m_result.paint_lost) {
outcome = TextureDisplacementPrepareOutcome::PaintLost;
return;
}
if (m_result.mesh.indices.empty()) {
outcome = TextureDisplacementPrepareOutcome::Unchanged;
return;
}
Plater *plater = wxGetApp().plater();
ModelVolume *volume = get_model_volume(m_input.volume_id, plater->model().objects);
// The lookup doubles as a staleness check: anything that replaces a volume's mesh also gives it a
// new id (set_new_unique_id()), so a prepare computed from a mesh that has since been replaced -
// by an undo, another bake, or a boolean - simply fails to find its volume and commits nothing.
if (volume == nullptr)
return;
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
{
Plater::TakeSnapshot snapshot(plater, m_input.snapshot_name, UndoRedo::SnapshotType::GizmoAction);
// The four standard paint channels ride across on ModelVolume's own spatial remap. The eight
// texture-displacement masks do not go through it - prepare_mesh() has already carried them,
// exactly, from the source triangles the refinement records - so they are put back after
// restore_painting(), which resets every extra facet before remapping the channels it knows.
std::optional<TriangleSelector::SavedPainting> saved_painting = volume->save_painting();
volume->set_mesh(TriangleMesh(std::move(m_result.mesh)));
volume->set_new_unique_id();
volume->calculate_convex_hull();
volume->restore_painting(saved_painting);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
volume->texture_displacement_facet(i).set_data(std::move(m_result.masks[size_t(i)]));
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
auto it = std::find(objs.begin(), objs.end(), object);
if (it != objs.end())
obj_list->update_info_items(size_t(it - objs.begin()));
}
plater->changed_object(*object);
}
outcome = TextureDisplacementPrepareOutcome::Committed;
}
void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input,
std::function<void(TextureDisplacementPrepareOutcome)> on_finished)
{
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementPrepareJob>(std::move(input), std::move(on_finished)));
}
} // namespace Slic3r::GUI

View File

@@ -0,0 +1,77 @@
#ifndef slic3r_TextureDisplacementPrepareJob_hpp_
#define slic3r_TextureDisplacementPrepareJob_hpp_
#include <functional>
#include <string>
#include <vector>
#include "Job.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
namespace Slic3r {
class ModelVolume;
}
namespace Slic3r::GUI {
// Getting a mesh ready to receive displacement - the isotropic remesh, carrying the paint onto it, and
// the adaptive refinement - off the UI thread.
//
// All three used to run inline behind a wxBusyCursor, which on any part big enough to matter meant tens
// of seconds with the window not repainting and no way to stop it: CGAL's remesher is single threaded
// and its cost grows with the square of 1/target_edge, and the refinement that follows spends a budget
// of hundreds of thousands of triangles. Indistinguishable from a hang, and reported as one.
//
// The work itself is GLGizmoTextureDisplacement::prepare_mesh(), which is pure - it takes an
// indexed_triangle_set and the layers' masks and returns new ones, touching no Model and no GUI - so all
// this job adds is the worker thread, the progress notification with its Cancel button, and the commit.
enum class TextureDisplacementPrepareOutcome
{
Committed, // the volume now carries the prepared mesh and the paint carried onto it
Unchanged, // nothing needed doing - the mesh already met the criteria; the model was not touched
PaintLost, // the remesh landed but no layer's paint survived it, so nothing was committed
Failed, // cancelled, threw, or the volume went away while the job ran
};
struct TextureDisplacementPrepareInput
{
ObjectID volume_id;
indexed_triangle_set base_mesh;
TextureDisplacementFacetsData masks;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementPrepareParams params;
// Captured on the main thread. Empty when no layer is colouring, in which case the refinement
// skips the colour criterion entirely. Only the *quantizer* is used here: refinement follows
// perceived colour, never the interleaving that realises a mix.
TextureColorSettings color;
// The undo step the commit opens. Standard mode's Bake names it after the bake, because the
// displacement job that follows commits into this same step rather than pushing its own.
std::string snapshot_name;
};
class TextureDisplacementPrepareJob : public Job
{
TextureDisplacementPrepareInput m_input;
TextureDisplacementPrepareResult m_result;
std::function<void(TextureDisplacementPrepareOutcome)> m_on_finished;
public:
TextureDisplacementPrepareJob(TextureDisplacementPrepareInput &&input,
std::function<void(TextureDisplacementPrepareOutcome)> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
};
// `on_finished` runs on the UI thread once the result has been committed (or found not to need
// committing), and always runs exactly once.
void queue_texture_displacement_prepare(TextureDisplacementPrepareInput &&input,
std::function<void(TextureDisplacementPrepareOutcome)> on_finished);
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementPrepareJob_hpp_

View File

@@ -0,0 +1,64 @@
#include "TextureDisplacementPreviewJob.hpp"
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
namespace Slic3r::GUI {
TextureDisplacementPreviewJob::TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished)
: m_input(std::move(input)), m_generation(generation), m_current_generation(std::move(current_generation)),
m_on_finished(std::move(on_finished))
{
}
void TextureDisplacementPreviewJob::process(Ctl &ctl)
{
// No ctl.update_status() anywhere in here on purpose - see the class comment. A preview is
// invisible bookkeeping; the only thing on screen should be the preview itself.
// Only ever touches m_input (captured by value before this job was queued) and local state -
// never the live Model - so this is safe to run concurrently with the UI thread.
TextureColorRequest color_request;
TextureColorRequest *color = nullptr;
if (!m_input.color.empty()) {
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
m_input.color.dither_cell_mm);
color_request.despeckle_passes = m_input.color.despeckle_passes;
color_request.out_triangle = &m_result.triangle_color;
if (color_request.quantize)
color = &color_request;
}
m_result.mesh = build_texture_displacement(m_input.base_mesh, m_input.layers, m_input.facets_data,
m_input.options,
[this, &ctl](int) {
// Bail the moment this preview stops being the current
// one; build_texture_displacement() then returns an
// empty mesh and finalize() drops it.
return !ctl.was_canceled() &&
(!m_current_generation ||
m_current_generation->load() == m_generation);
},
color, m_input.volume_to_world);
}
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
{
if (!m_on_finished)
return;
// The handler must run on *every* outcome, cancellation included, because the caller uses it to
// clear its "a job is in flight" latch. Returning early on `canceled` - which is what a cancel_all()
// from Plater (project load/close, app exit) delivers - left that latch stuck true and no preview
// was ever queued again for the rest of the session. An empty result is the caller's signal that
// nothing usable came back; it already handles that.
if (canceled || eptr)
m_on_finished(TextureDisplacementPreviewResult{}, m_generation);
else
m_on_finished(std::move(m_result), m_generation);
}
} // namespace Slic3r::GUI

View File

@@ -0,0 +1,82 @@
#ifndef slic3r_TextureDisplacementPreviewJob_hpp_
#define slic3r_TextureDisplacementPreviewJob_hpp_
#include <atomic>
#include <cstdint>
#include <functional>
#include <memory>
#include <vector>
#include "libslic3r/Color.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "Job.hpp"
namespace Slic3r::GUI {
// Everything process() needs, captured by value on the main thread when the job is queued -
// mirrors TextureDisplacementBakeInput, but a preview never writes back to the Model.
struct TextureDisplacementPreviewInput
{
indexed_triangle_set base_mesh;
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementFacetsData facets_data;
TextureDisplacementOptions options;
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
// is and the two cannot disagree. See build_texture_displacement().
Transform3d volume_to_world = Transform3d::Identity();
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
TextureColorSettings color;
};
// A preview result: the displaced mesh, and - when the input carried a palette - one filament index
// per triangle (an EnforcerBlockerType value; 0 means "no colour from the texture").
struct TextureDisplacementPreviewResult
{
indexed_triangle_set mesh;
std::vector<uint8_t> triangle_color;
};
// Computes the true (unbaked) displaced-mesh preview in the background. With several painted
// layers this is real, non-trivial CPU work (PNG sampling, per-layer vertex welding), which used
// to run synchronously on every paint stroke and parameter tweak and made editing feel slow with
// more than one or two layers. Unlike Bake, this never touches the live Model - a preview is
// purely informational, there is nothing to commit.
//
// Deliberately reports no status: the Job framework turns the first update_status() call into an
// on-screen progress notification, and a preview firing one on every stroke and slider release
// buried the user in notifications that only closed at 100%.
class TextureDisplacementPreviewJob : public Job
{
public:
// `generation` is an opaque token the caller controls (typically an incrementing counter):
// on_finished should only actually be applied by the caller if it still matches the caller's
// current generation when the job completes, so that a burst of edits queuing several of
// these jobs in a row can't have an earlier, now-stale result clobber a later one that
// finishes first.
//
// `current_generation` is the caller's live counter, shared with the worker thread. The job
// polls it *while computing* and aborts as soon as it no longer matches - so a preview that has
// already been superseded stops burning CPU instead of running to completion for a result that
// will only be thrown away. That matters because the UI job worker runs one job at a time in FIFO
// order: without it, a Bake queued behind a handful of stale previews waits for every one of them.
TextureDisplacementPreviewJob(TextureDisplacementPreviewInput &&input, uint64_t generation,
std::shared_ptr<const std::atomic<uint64_t>> current_generation,
std::function<void(TextureDisplacementPreviewResult, uint64_t)> on_finished);
void process(Ctl &ctl) override;
void finalize(bool canceled, std::exception_ptr &eptr) override;
private:
TextureDisplacementPreviewInput m_input;
uint64_t m_generation;
std::shared_ptr<const std::atomic<uint64_t>> m_current_generation;
TextureDisplacementPreviewResult m_result;
std::function<void(TextureDisplacementPreviewResult, uint64_t)> m_on_finished;
};
} // namespace Slic3r::GUI
#endif // slic3r_TextureDisplacementPreviewJob_hpp_

Some files were not shown because too many files have changed in this diff Show More