Compare commits

..
Author SHA1 Message Date
ExPikaPaka 18d891f7c7 Merge remote-tracking branch 'origin/main' into feature/texdisp-model-uv
# Conflicts:
#	src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
2026-10-12 15:15:34 +02:00
ExPikaPaka d2fff6dec9 Take the tile-size row through the typed slider's tooltip argument
The typed-slider branch merged, so float_row() now carries each row's
tooltip itself and shows it for a disabled slider as well. The ModelUV row
hands it the two texts instead of drawing its own hover.
2026-10-12 15:14:50 +02:00
SoftFever 7149728219 Fix clang-cl dependency builds failing on a precompiled header from the compiler cache 2026-10-12 20:27:37 +08:00
ExPikaPaka 496b16dabb Keep the model-UV work off the lines its neighbours change
Three small moves, no behaviour change:

- the two 'case ModelUV:' labels are dropped. They fell through to
  Triplanar, which is what 'default:' already does, and they sat one line
  under the return statement the UV-orientation branch rewrites.
- transfer_facet_paint() moves to its own place in the file, away from
  transfer_uv_channel(), so the branch that carries paint across a bake can
  hold the same function verbatim.
- the UV channel is put back after the layer masks are reset rather than
  before, which keeps it clear of the paint transfer above it.

The comment over the colour merge is also corrected: the carried paint is
what it now merges over.
2026-10-12 14:11:15 +02:00
SoftFever f49d49b5a6 Texture displacement: typed values on every panel slider (#16395)
Every slider in the texture displacement panel can now be given an exact
number, either by Ctrl+clicking the track or via a small edit icon at
its right end. Both routes reuse ImGui's own temp-input mode.

Each slider also gets a tooltip with its description and its range, and
the panel's `?` popup lists both ways in.

Draft: description to be refined.
2026-10-12 20:03:43 +08:00
SoftFever 08f2037cbd Texture displacement: save layers, textures and paint in the .3mf (#16148)
## General

Nothing about texture displacement was written to the `.3mf`, so a
painted relief did not survive closing the project.
Everything the user set up is saved now, so a project reopens with the
relief intact and still editable.

### What is saved
- The layer stack: every per-layer setting (depth, midlevel, smoothing,
tiling, projection blend mode, colour) and the per-volume options
- Each layer's texture, as a file in the archive (why? because user
might want to move .3mf to another computer where custom textures are
missing, or he saves .3mf then deletes textures, and returns back to
.3mf)
- The painted region, one mask per layer slot
- The UV work: LSCM seams, island placement, per-vertex overrides
- Editor state: the selected layer and Standard/Pro, so the panel
reopens where it was left

### What is not saved
- The preview mesh. It is a second copy of the model in the file, for
something the app rebuilds quickly on reopen.
- Panel docked/undocked, which goes to the application config instead

## Format

- `<volume id>.json` - the layer stack and the per-volume options
- `<volume id>_<slot>.png` - each layer's texture, referenced from the
JSON through `path_in_3mf`

Paint masks ride along as one per-triangle attribute per slot
(`paint_texture_0..7`), beside
the attributes the existing paint tools already write.

## Compatibility
Both ways:
- an older build ignores what it does not know and still opens the
project, losing only the relief
- a newer build finds nothing in an older project and leaves the volume
as today.
2026-10-12 20:00:17 +08:00
SoftFever d8cae3268a Merge branch 'main' into feature/texdisp-3mf-persistence 2026-10-12 19:47:05 +08:00
HanifKoh cccf53850e Remove Never-Built and Stub Source Files (#16361)
SysInfoDialog, JumpPointSearch and WebUpdatePlugin are in no CMake list on any platform, and nothing includes ConfigExceptions.hpp, wxinit.h or Utils/Profile.hpp. MultiTaskModel holds only empty namespaces. Also drop the empty GUI_App::system_info() and comments that still mention SysInfoDialog.
2026-10-12 18:53:32 +08:00
HanifKoh 5095733323 Fix Include Guards Copied From Other Headers (#16368)
TriangleMeshDeal.hpp reused Timer.hpp's guard and CloneDialog.hpp reused
SingleChoiceDialog.hpp's, so including both headers of a pair silently
dropped the second one. Use #pragma once instead.
2026-10-12 18:52:53 +08:00
HanifKoh 0cec10748e Remove the /bot fix-tidy clang-tidy Fix Bot (#16400)
Its inline suggestions rarely apply: clang-tidy adds a missing include
to the include block at the top of the file, which a PR seldom touches,
and GitHub only accepts review suggestions on lines in the PR's diff.
2026-10-12 18:47:26 +08:00
HanifKoh c55f9c2251 Add a /bot fix-tidy Command That Suggests clang-tidy's Missing Includes (#16398)
* Add a /bot fix-tidy Command That Pushes clang-tidy's Missing Includes

A maintainer comments /bot fix-tidy on a PR. A read-only job runs
scripts/run_clang_tidy.sh --fix on the PR's merge commit and returns the
patch; a second job, which never runs the PR's code, accepts it only when
it adds nothing but #include lines to C++ files the PR already changes,
then commits it on the PR head and pushes with CLANG_TIDY_BOT_TOKEN.

* Post clang-tidy's Fix as Review Suggestions Instead of Pushing It

A failed clang-tidy check now runs once more with --fix and uploads the
patch as the clang-tidy-fix artifact; the check still fails. /bot
fix-tidy downloads that patch for the PR's latest commit, checks it adds
only includes to C++ files the PR changes, and posts one review: a
one-click suggestion for each include on a line the PR's diff shows, and
the whole patch with a git apply command. Nothing is pushed and no PR
code runs in the bot, so the PR's author can run it too and no token is
needed.

* Make the clang-tidy Fix in Its Own PR-Scoped Workflow on Request

clang_tidy.yml is back to main's version. clang_tidy_fix.yml runs on
every PR push but skips its job on the first attempt; /bot fix-tidy
re-runs it, so run_clang_tidy.sh --fix runs in the PR's own context
with a read-only token and the PR's cache, never main's, and the PR's
author can ask for it. When that re-run finishes, the bot posts the
patch as review suggestions as before. When the clang-tidy step fails,
the bot also comments asking the author to run the script or the
command, edits that comment on later failures, and deletes it once the
check passes.

* Give the clang-tidy Bot Write Access to PRs It Answers

* Tell /bot fix-tidy Users How to Start a Fix Run When There Is None to Re-Run
2026-10-12 18:31:03 +08:00
SoftFever 3f78959a0c Harden loading and saving of texture displacement in projects
A project no longer reads texture files from outside its archive, and a
malformed value or layer slot drops only the texture layers instead of
failing the whole load. Copies of an object that share a mesh keep their
own texture paint when saved, and projects without texture paint no
longer use extra memory to load.
2026-10-12 17:47:04 +08:00
SoftFever d2675f15d1 Fix Qidi start G-code ignoring the selected plate type (#16372)
# Description

With "Support multi bed types" enabled on a Qidi printer, the
first-layer bed temperature was always taken from the Smooth PEI / High
Temp Plate setting, whichever plate type was selected.

The slicer computes the right value (`first_layer_bed_temperature` in
the G-code footer follows the selected plate), but the machine start
G-code of these presets hardcoded `[hot_plate_temp_initial_layer]`:

- `fdm_qidi_x3_common` (base of X-Max 3, X-Plus 3, X-Smart 3, X-Plus 4,
Q1 Pro)
- `Qidi Q1 Pro 0.4 nozzle`
- `Qidi X-CF Pro 0.4 nozzle`

They now use `[bed_temperature_initial_layer_single]`, which follows the
selected plate type, as most other profiles do. The Qidi bundle version
is bumped to `02.04.00.17`.

With "Support multi bed types" disabled, these printers use the default
plate (Smooth PEI / High Temp Plate), so the temperature is unchanged
there.

# Screenshots/Recordings/Graphs

N/A — profile change only.

## Tests

`python3 scripts/orca_profile_tool.py check` passes.

Sliced on Qidi X-Max 3 0.4 nozzle with "Support multi bed types"
enabled, `0.20mm Standard @Qidi XMax3`, `Generic PLA @Qidi`, with a
different bed temperature set for each plate type (first layer / other
layers).

Before, with first layer set to 50–55 °C: `M190 S54` for every plate
type.

After, a two-layer print:

| Plate type | Set (first / other) | `M190` at start | `M140` at layer 2
|
| --- | --- | --- | --- |
| Cool Plate SuperTack | 51 / 61 | 51 | 61 |
| Cool Plate | 52 / 62 | 52 | 62 |
| Textured Cool Plate | 53 / 63 | 53 | 63 |
| Engineering Plate | 54 / 64 | 54 | 64 |
| Smooth PEI / High Temp Plate | 55 / 65 | 55 | 65 |
| Textured PEI Plate | 56 / 66 | 56 | 66 |

🤖 Generated with [Claude Code](https://claude.com/claude-code)4
2026-10-12 17:14:22 +08:00
HanifKoh 5371c85362 Refresh Dark Mode Colours When the System Theme Differs at Startup (#16390)
On Linux and macOS the app follows the system theme: at startup it
overwrites the saved dark_color_mode with the system appearance. The label
colours and the cached dark mode flag were already set from the saved value
by then, and the later "changed" check reads the mode only after the
overwrite, so it never fires. For that session every dialog that maps its
colours through UpdateDlgDarkUI keeps the old mode, e.g. white dialogs on a
dark main window after switching the desktop to dark, or on a fresh install
on a dark desktop.

Refresh the label colours and the flag when the system theme differs from
the saved mode.
2026-10-12 17:09:05 +08:00
SoftFever 30e48183e3 Merge branch 'main' into feature/texdisp-3mf-persistence
# Conflicts:
#	src/libslic3r/TextureDisplacement.hpp
#	tests/libslic3r/test_texture_displacement.cpp
2026-10-12 16:58:03 +08:00
SoftFever a243941b72 Add favorite filaments to AMS filament dropdown (#13946)
# Description

## Summary:

Adds favorite filament support to the AMS material selection dropdown.

Users can now click a star icon next to a selectable filament in the AMS Materials Setting dialog to mark it as a favorite. Favorited filaments appear in a new `Favorite filaments` section at the top of the dropdown, while all compatible filaments remain available in the `All filaments` section.

This PR also adds generic right-side action icon support to Orca's custom dropdown widget:

- Adds optional action icons to `DropDown::Item`.
- Adds a new `EVT_DROPDOWN_ITEM_ACTION` event for clicks on the action icon.
- Relays the action event through `ComboBox`.
- Adds `ComboBox::RefreshDropdownIfOpen()` so callers can refresh an already-open dropdown without changing normal `ForceDropdownOpen()` behavior.

The AMS favorites implementation:

- Uses existing bundled star assets:
  - `score_star_dark` for non-favorited filaments
  - `score_star_light` for favorited filaments
- Stores favorite filament IDs in app config under `ams_favorite_filament_ids`.
- Stores stable filament IDs instead of display names
- Uses existing C-style string escaping helpers for persistence.
- Keeps Bambu/RFID/read-only slot behavior unchanged

No new external dependencies.

**Note:** `AMSMaterialsSetting` now uses Orca's custom `ComboBox` on macOS as well, because the favorite-star UI depends on the custom `DropDown` item renderer. I was able to test this on Windows, but macOS/Linux smoke testing is still needed and would be appreciated.

# Screenshots/Recordings:

https://github.com/user-attachments/assets/41e881be-d4de-4a7d-8f99-4297567adef2

<img width="494" height="36" alt="Screenshot 2026-05-30 103432" src="https://github.com/user-attachments/assets/e7c34ed3-db7a-4705-a1cd-4bfe7ac1616a" />


## Tests

Tested on Windows:

- Built OrcaSlicer locally
- Opened the Device tab with Bambu Lab printer with AMS
- Opened the AMS Materials Setting dialog from an editable AMS slot
- Confirmed clicking a star toggles favorite state.
- Star clicks do not select the filament row.
- Dropdown remains open after toggling a favorite.
- Confirmed favorited filaments appear under `Favorite filaments`.
- All compatible filaments remain visible under `All filaments`.
- Confirmed Filament still selected as before
- Confirmed favorites persist after closing and reopening the dialog.
- Confirmed favorites persist after restarting OrcaSlicer.
- Confirmed the favorite IDs are saved in `OrcaSlicer.conf` under `ams_favorite_filament_ids`.
- Bambu/RFID/read-only filament slots continue using the existing read-only display behavior.

Not tested:

- macOS
- Linux/Wayland

These should be tested, `ComboBox` behavior not tested on macOS.

[How to Download Pull Requests Artifacts for Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-10-12 16:37:29 +08:00
SoftFever 6a1a5d51e9 Show an outline star for filaments that are not favorites 2026-10-12 16:03:49 +08:00
HanifKoh e625845b34 Keep Snapshot Suppression for the Whole Object List Edit (#16388)
Five Plater::TakeSnapshot guards in GUI_ObjectList were unnamed temporaries, so snapshots were allowed again as soon as the snapshot was taken. Naming them keeps the suppression until the edit is done, as everywhere else.
2026-10-12 16:02:43 +08:00
ExPikaPaka c113b46594 Include cstdint and Point.hpp where the new code uses uint8_t and Vec2f
The clang-tidy job checks include-what-you-use on changed lines, and the three
files it named relied on a transitive include for a symbol the new code
introduced.
2026-10-12 09:44:26 +02:00
SoftFever 01b340dee1 Fix regressions in the AMS filament favorites dropdown 2026-10-12 14:54:17 +08:00
ExPikaPaka 18cef4f51d Texture a model through its own UV channel, and keep its colours through a bake
Importing a model that already carries colours was broken for texture
displacement in two ways: the UV layout it was authored against was thrown
away and re-unwrapped, and a bake overwrote the colours it arrived with.

A model's own UV channel is now kept. ModelVolume::texture_uv holds three uvs
per triangle, in the mesh's own triangle order and with V flipped to the
convention DecodedHeightTexture samples in. It is filled from a textured OBJ's
vt data on import and carried across every stage that replaces the mesh - the
colour import's painted mesh, the gizmo's remesh and subdivide, the default
pipeline's rebuild - by transfer_uv_channel(), which gives each new corner the
uv of its closest point on the surface it came from. A new mapping,
TextureProjectionMethod::ModelUV, samples that channel instead of solving an
unwrap, and a new layer on a volume that has one starts there. Its uv is
already normalized, so it keeps the layer's rotation and offset but not the
millimetre tile size (the slider is disabled) or the non-square aspect
correction; with no channel it falls back to Triplanar, as LSCM does when a
patch will not flatten.

A bake with colour off on every layer now leaves the model's colours where
they were. The default pipeline rebuilds the topology, which left
mmu_segmentation_facets describing a triangle set that no longer existed.
ModelVolume::restore_painting() cannot carry it: its remap wants a target
triangle overlapping the source paint to within 0.01 mm, and a baked surface
has moved by the relief depth. transfer_facet_paint() gives each new facet the
state at its own foot point on the pre-bake surface instead, and the bake
commit runs it over all four paint channels. The classic path is untouched,
since there triangle i is still triangle i.

Blend gains an Override entry next to Add: the layer replaces the displacement
under it rather than combining with it, which is what the stack's colour
already does where two layers overlap. The combo's order is the panel's, not
the enum's, so stored blend modes are unaffected.

Measured on a textured OBJ of 85224 triangles (uv_color_probe in the bake
harness): the channel survives import intact, the relief follows the model's
own uv on 98.7% of vertices against 51.3% for a re-unwrap, and a colour-off
bake through the default pipeline keeps the colours on 99.8% of the baked
surface where it previously kept none. Through a coloured 3mf, 100%.
2026-10-12 07:57:15 +02:00
ExPikaPaka ed1f3e52c9 Texture displacement: typed values on every panel slider
Every slider in the texture displacement panel now takes an exact number,
either by Ctrl+clicking the track or through a small edit icon at its right
end. Both routes end in ImGui's own temp-input mode: the icon only asks ImGui
to focus the slider, which is what turns it into an input box, so the parsing,
the clamping and the Enter/click-away behaviour stay ImGui's rather than
becoming a second implementation.

The mechanism lives in one helper, typed_row(), with a float and an int form
over it. float_row() and int_row() are thin wrappers on those, and the six
sliders that previously built their widget inline (brush size, smart fill
angle, edge fade amount, remesh sharp angle, refine resolution and the uniform
triangle budget) now go through the same path.

Each slider carries a tooltip with its description and its range. The range is
derived from the bounds and the format string the slider is already given, so
no row states them twice and the tooltip cannot drift from the track. The
tooltip allows hovering while disabled, since several rows are drawn inside
disabled_begin(), where a greyed slider is exactly when the explanation is
wanted.

The panel's "?" popup gains an entry for each of the two ways in, appended to
every tool's shortcut list and shown on their own when no painting tool is
selected, where that popup was previously empty.
2026-10-12 07:57:07 +02:00
SoftFever 00d11a7ba0 Merge branch 'main' into UI-changes 2026-10-12 12:17:21 +08:00
Boris SamorodovandClaude Opus 5.5 546b9c621d Fix Qidi start G-code ignoring the selected plate type
The start G-code of fdm_qidi_x3_common, Qidi Q1 Pro 0.4 nozzle and
Qidi X-CF Pro 0.4 nozzle heated the bed to hot_plate_temp_initial_layer,
so with "Support multi bed types" enabled every plate type got the
Smooth PEI / High Temp Plate temperature. Use
bed_temperature_initial_layer_single, which follows the selected plate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 00:49:25 +02:00
ExPikaPaka 8ecfee6a21 Save where the panel was left, and recover a texture the archive lacks
The project carried the layer stack but not the user's place in it: a reopened
project landed on slot 0 in Standard, whatever they had been editing. The active
layer and Standard/Pro now live on the volume, next to the rest of its options,
and come back with it. Docked/undocked goes to the application config instead -
it describes the user's window, not the model, so opening someone else's project
must not move their panel.

A layer whose image_data was empty at save time was written with no texture and
came back with none. It now falls back to the file the layer came from, which the
importer put in the user's own texture library, so a project reopened on the same
machine keeps its pixels.

The texture is always stored as .png: the importer converts whatever the user
picked, so the extension was being taken from a name that no longer described the
bytes.

Also adds v2_flip_edges to the cereal field list, which the undo stack was
silently dropping.
2026-10-09 09:35:35 +02:00
ExPikaPaka fbf4652e12 Fix the build and clang-tidy failures in the persistence change
The round-trip test named a TextureTileMethod enumerator that does not
exist (MirroredRepeat, not Mirror) and used a bare Approx, which Catch2 v3
keeps in its own namespace. The rest of the file compares floats with
WithinAbs, so these follow it rather than reaching for Catch::Approx.

bbs_3mf.cpp reads and writes the layer JSON but picked up the declarations
through another header, and the same for boost::to_lower.
2026-10-09 09:05:42 +02:00
ExPikaPaka be9ff50130 Merge remote-tracking branch 'origin/main' into feature/texdisp-3mf-persistence
# Conflicts:
#	src/libslic3r/TextureDisplacement.cpp
2026-10-09 09:03:51 +02:00
SoftFever dee1f1715f Merge branch 'main' into feature/texdisp-3mf-persistence 2026-10-09 12:15:16 +08:00
ExPikaPaka fcb1f62bd9 Save texture displacement in the project file
A painted relief did not survive closing the project: nothing about the feature
was written to the .3mf, so the layers, their textures and the paint mask were
all lost on save. The layer struct's cereal save()/load() serve the undo/redo
stack only, which is one session and one binary stream.

Write the layer stack and the per-volume options as JSON in the archive, and
each layer's texture as the image file it was loaded from, both referenced by
path - the same split EmbossShape already makes for its SVG. The image stays
out of the XML deliberately: it is binary and routinely megabytes, and base64
in an attribute would bloat the one file every reader parses just to list the
objects. JSON rather than the cereal stream next to it because that one is
positional and unversioned, which would make every future field a
project-breaking change; an unknown key is ignored and a missing one keeps its
default, so a project written by either side keeps loading.

The paint masks go in as one attribute per layer slot, mirroring paint_color.
Older readers ignore attributes they do not know, so a project written here
still opens in a build without the feature - it loses the relief, which is all
it could have done with it anyway.

The layers are restored while the volume is being built rather than after the
archive is walked: volumes are constructed only once the whole archive has been
read, so a hook placed after the walk ran before any volume existed and
restored nothing.
2026-10-05 08:36:22 +02:00
SoftFever a13089d357 Merge branch 'main' into UI-changes 2026-06-02 14:40:05 +08:00
KrishManan fb6c93963d Add AMS filament favorites 2026-05-30 17:45:03 +08:00
KrishManan 7688f7a16c Add dropdown item action icons 2026-05-30 17:45:03 +08:00
50 changed files with 2434 additions and 1065 deletions
+8
View File
@@ -1,10 +1,18 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style
# depfile, so a cache hit writes the object and no depfile, and Ninja then records
# no headers for that object. ccache reproduces /showIncludes output on a hit.
#
# Clang also records the modification time of every input in a precompiled header
# and rejects the header once one differs. ccache caches the compile that creates
# the header, so a tree that regenerates cmake_pch.cxx gets back a header stamped
# with the old file's time, and every compile that uses it fails. The build system
# already rebuilds the header when an input changes. This covers the dependencies,
# such as wxWidgets; PrecompiledHeader.cmake does the same for OrcaSlicer's targets.
foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif ()
endforeach ()
+3
View File
@@ -0,0 +1,3 @@
<svg width="28" height="27" viewBox="0 0 28 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path transform="matrix(0.9 0 0 0.9 1.4 1.35)" d="M11.9888 2.01601C12.8984 0.494662 15.1024 0.494662 16.012 2.01601L18.8677 6.79237C19.1958 7.34113 19.7339 7.73201 20.3572 7.87447L25.7821 9.11446C27.5101 9.50943 28.1913 11.6056 27.0254 12.9408L23.3652 17.1327C22.9449 17.6143 22.7394 18.2469 22.7964 18.8836L23.2936 24.4263C23.4519 26.1918 21.6687 27.4873 20.0386 26.7911L14.9209 24.6056C14.3329 24.3545 13.6679 24.3545 13.0799 24.6056L7.96209 26.7911C6.33196 27.4873 4.54887 26.1918 4.70722 24.4263L5.20434 18.8836C5.26145 18.2469 5.05595 17.6143 4.63544 17.1327L0.97533 12.9408C-0.19048 11.6056 0.490599 9.50943 2.21859 9.11446L7.64364 7.87447C8.2669 7.73201 8.80494 7.34113 9.13302 6.79237L11.9888 2.01601Z" stroke="#949494" stroke-width="2.4" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 876 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path d="M2.5,2.5h11v11h-11z" style="fill:none;stroke:#949494;stroke-linecap:round;stroke-linejoin:round"/><path d="M8,2.5v11M2.5,8h11" style="fill:none;stroke:#949494;stroke-dasharray:1.5,1.5"/><path d="M4.5,11.5l3.5-6l3.5,6z" style="fill:#009688"/></svg>

After

Width:  |  Height:  |  Size: 340 B

+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "Qidi",
"version": "02.04.00.16",
"version": "02.04.00.17",
"force_update": "0",
"description": "Qidi configurations",
"machine_model_list": [
@@ -83,7 +83,7 @@
"extruder_clearance_height_to_lid": "120",
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"machine_start_gcode": "PRINT_START BED=[hot_plate_temp_initial_layer] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[hot_plate_temp_initial_layer]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"machine_start_gcode": "PRINT_START BED=[bed_temperature_initial_layer_single] HOTEND=[nozzle_temperature_initial_layer] CHAMBER=[chamber_temperature]\nM83\nM140 S[bed_temperature_initial_layer_single]\nM104 S[nozzle_temperature_initial_layer]\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1] - 5, first_layer_print_min[1] + 80) - 85), 0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 3} Z0\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85), 0) + 6}\nG1 Z1 F600\n",
"thumbnails_format": "PNG",
"default_filament_profile": [
"Generic PLA @Qidi Q1 Pro 0.4 nozzle"
@@ -100,7 +100,7 @@
"default_filament_profile": [
"Generic PLA @Qidi"
],
"machine_start_gcode": "G28\nM140 S[hot_plate_temp_initial_layer]\nM190 S[hot_plate_temp_initial_layer]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_start_gcode": "G28\nM140 S[bed_temperature_initial_layer_single]\nM190 S[bed_temperature_initial_layer_single]\nM109 S[nozzle_temperature_initial_layer]\nG92 E-19\nG0 Y5 Z0.3 F3600\nG1 X5 E0 F2400\n",
"machine_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\n",
"scan_first_layer": "0"
}
@@ -24,7 +24,7 @@
"210x210/COLPIC",
"110x110/PNG"
],
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[hot_plate_temp_initial_layer]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_start_gcode": "PRINT_START\nG28\nM141 S0\nG0 Z50 F600\nM190 S[bed_temperature_initial_layer_single]\nG28 Z\nG29; mesh bed leveling ,comment this code to close it\nG0 X0 Y0 Z50 F6000\nM141 S{overall_chamber_temperature}\nM109 S[nozzle_temperature_initial_layer]\nM106 P3 S255\nM83\nG4 P3000\nG0 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0)} Z5 F6000\nG0 Z[initial_layer_print_height] F600\nG1 E3 F1800\nG1 X{(min(print_bed_max[0], first_layer_print_min[0] + 80))} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0)} E{85 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 85} E{83 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 2} E{2 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 Y{max((min(print_bed_max[1], first_layer_print_min[1] + 80) - 85),0) + 3} E{82 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 X{max((min(print_bed_max[0], first_layer_print_min[0] + 80) - 85),0) + 12} E{-10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\nG1 E{10 * 0.5 * initial_layer_print_height * nozzle_diameter[0]} F3000\n",
"machine_end_gcode": "M141 S0\nM104 S0\nM140 S0\nG1 E-3 F1800\nG0 Z{min(max_print_height, max_layer_z + 3)} F600\nG0 X0 Y0 F12000\n{if max_layer_z < max_print_height / 2}G1 Z{max_print_height / 2 + 10} F600{else}G1 Z{min(max_print_height, max_layer_z + 3)}{endif}",
"scan_first_layer": "0"
}
+166 -2
View File
@@ -22,6 +22,7 @@
#include "libslic3r/BrimEarsPoint.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/CustomGCode.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r_version.h"
#include "bbs_3mf.hpp"
@@ -354,6 +355,13 @@ static constexpr const char* CUSTOM_SUPPORTS_ATTR = "paint_supports";
static constexpr const char* CUSTOM_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color";
// Texture displacement. One paint mask per layer slot, mirroring paint_color; the layer stack itself is
// a JSON file in the archive, named by the volume metadata key below (see add_texture_displacement()).
static constexpr const char* TEXTURE_DISPLACEMENT_ATTRS[Slic3r::TEXTURE_DISPLACEMENT_MAX_LAYERS] = {
"paint_texture_0", "paint_texture_1", "paint_texture_2", "paint_texture_3",
"paint_texture_4", "paint_texture_5", "paint_texture_6", "paint_texture_7" };
static constexpr const char* TEXTURE_DISPLACEMENT_KEY = "texture_displacement";
static constexpr const char* TEXTURE_DISPLACEMENT_DIR = "Metadata/texture_displacement/";
// BBS
static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
@@ -847,6 +855,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::vector<std::string> custom_seam;
std::vector<std::string> mmu_segmentation;
std::vector<std::string> fuzzy_skin;
// One per texture displacement layer slot, indexed like `triangles` but stored only as far as the
// slot's last painted triangle: empty when the slot has no paint, which is nearly every file.
std::vector<std::string> texture_displacement[TEXTURE_DISPLACEMENT_MAX_LAYERS];
// BBS
std::vector<std::string> face_properties;
@@ -867,6 +878,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
custom_seam.clear();
mmu_segmentation.clear();
fuzzy_skin.clear();
for (std::vector<std::string> &slot : texture_displacement)
slot.clear();
}
};
@@ -1200,6 +1213,11 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
/*IdToSlaSupportPointsMap m_sla_support_points;
IdToSlaDrainHolesMap m_sla_drain_holes;*/
PathToEmbossShapeFileMap m_path_to_emboss_shape_files;
// Texture displacement: every file under Metadata/texture_displacement/, by archive path. Volumes
// are built only after the whole archive has been walked, so by the time a volume names its JSON
// every file it could refer to is already in here, whatever order the archive happened to be in.
std::map<std::string, std::string> m_texture_displacement_files;
void _apply_texture_displacement(ModelVolume &volume, const std::string &json_path);
std::string m_curr_metadata_name;
std::string m_curr_characters;
std::string m_name;
@@ -2073,6 +2091,13 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (_is_svg_shape_file(name)) {
_extract_embossed_svg_shape_file(name, archive, stat);
}
else if (boost::algorithm::istarts_with(name, TEXTURE_DISPLACEMENT_DIR)) {
std::string contents(stat.m_uncomp_size, '\0');
if (mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, contents.data(), stat.m_uncomp_size, 0))
m_texture_displacement_files.emplace(name, std::move(contents));
else
add_error("Error while reading texture displacement data");
}
else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
m_parsing_slice_info = true;
//extract slice info from archive
@@ -3310,6 +3335,45 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}*/
// Restores one volume's texture displacement stack from the JSON it named. Called while the volume is
// being built, which is after the whole archive has been walked - so every file it can refer to is
// already extracted, and no deferral is needed.
//
// A missing or malformed JSON leaves the volume with no layers but keeps the paint mask it already
// loaded, which is the same state as a project saved by a build without the feature: recoverable by
// picking the texture again rather than a hard failure.
void _BBS_3MF_Importer::_apply_texture_displacement(ModelVolume &volume, const std::string &json_path)
{
const auto json = m_texture_displacement_files.find(json_path);
if (json == m_texture_displacement_files.end()) {
add_error("Missing texture displacement data: " + json_path);
return;
}
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementOptions options;
if (!texture_displacement_layers_from_json(json->second, layers, options)) {
add_error("Malformed texture displacement data: " + json_path);
return;
}
// The archive is the only source of a layer's pixels. `path` comes from the project file, so loading
// the project must never read the file it names: that would let whoever made the project pick any
// local (or, on Windows, network) file to read, and the next save would embed it in the archive.
// `path` stays a label of where the texture came from; a layer the archive carries no image for
// loads without one, its settings intact, until the user picks a texture again.
for (TextureDisplacementLayer &layer : layers) {
if (layer.path_in_3mf.empty())
continue;
const auto image = m_texture_displacement_files.find(layer.path_in_3mf);
if (image == m_texture_displacement_files.end()) {
add_error("Missing texture displacement image: " + layer.path_in_3mf);
continue;
}
layer.image_data = std::make_shared<std::vector<unsigned char>>(image->second.begin(), image->second.end());
}
volume.texture_displacement_layers = std::move(layers);
volume.texture_displacement_options = options;
}
void _BBS_3MF_Importer::_extract_embossed_svg_shape_file(const std::string &filename, mz_zip_archive &archive, const mz_zip_archive_file_stat &stat){
assert(m_path_to_emboss_shape_files.find(filename) == m_path_to_emboss_shape_files.end());
auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
@@ -3966,6 +4030,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
m_curr_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
m_curr_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
m_curr_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = m_curr_object->geometry.texture_displacement[slot];
mask.resize(m_curr_object->geometry.triangles.size());
mask.back() = paint;
}
// BBS
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
}
@@ -5317,6 +5387,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->mmu_segmentation_facets.touch();
volume->fuzzy_skin_facets.shrink_to_fit();
volume->fuzzy_skin_facets.touch();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const std::vector<std::string> &mask = sub_object->geometry.texture_displacement[slot];
if (mask.empty())
continue; // no triangle painted on this slot
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
facets.reserve(triangles_count);
for (size_t i = 0; i < triangles_count && i < mask.size(); ++i)
if (!mask[i].empty())
facets.set_triangle_from_string(i, mask[i]);
facets.shrink_to_fit();
facets.touch();
}
}
volume->set_type(volume_data->part_type);
@@ -5332,6 +5414,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
for (const Metadata& metadata : volume_data->metadata) {
if (metadata.key == NAME_KEY)
volume->name = metadata.value;
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
_apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format
@@ -5496,12 +5580,28 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->seam_facets.shrink_to_fit();
volume->mmu_segmentation_facets.shrink_to_fit();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const std::vector<std::string> &mask = geometry.texture_displacement[slot];
if (mask.empty())
continue; // written by a build without the feature
FacetsAnnotation &facets = volume->texture_displacement_facet(slot);
facets.reserve(triangles_count);
for (size_t i = 0; i < triangles_count; ++i) {
const size_t index = volume_data.first_triangle_id + i;
if (index < mask.size() && !mask[index].empty())
facets.set_triangle_from_string(i, mask[index]);
}
facets.shrink_to_fit();
facets.touch();
}
volume->set_type(volume_data.part_type);
// apply the remaining volume's metadata
for (const Metadata& metadata : volume_data.metadata) {
if (metadata.key == NAME_KEY)
volume->name = metadata.value;
else if (metadata.key == TEXTURE_DISPLACEMENT_KEY)
_apply_texture_displacement(*volume, metadata.value);
//else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format
@@ -5805,6 +5905,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
current_object->geometry.custom_seam.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_SEAM_ATTR));
current_object->geometry.mmu_segmentation.push_back(bbs_get_attribute_value_string(attributes, num_attributes, MMU_SEGMENTATION_ATTR));
current_object->geometry.fuzzy_skin.push_back(bbs_get_attribute_value_string(attributes, num_attributes, CUSTOM_FUZZY_SKIN_ATTR));
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
if (const char *paint = bbs_get_attribute_value_charptr(attributes, num_attributes, TEXTURE_DISPLACEMENT_ATTRS[slot])) {
std::vector<std::string> &mask = current_object->geometry.texture_displacement[slot];
mask.resize(current_object->geometry.triangles.size());
mask.back() = paint;
}
// BBS
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
}
@@ -6808,6 +6914,9 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n";
stream << " <Default Extension=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\n";
stream << " <Default Extension=\"jpg\" ContentType=\"image/jpeg\"/>\n";
stream << " <Default Extension=\"jpeg\" ContentType=\"image/jpeg\"/>\n";
stream << " <Default Extension=\"json\" ContentType=\"application/json\"/>\n";
stream << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
stream << "</Types>";
@@ -7232,10 +7341,14 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (iter != m_shared_meshes.end())
{
const ModelVolume* shared_volume = iter->second.second;
bool same_texture_paint = true;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS) && same_texture_paint; ++slot)
same_texture_paint = shared_volume->texture_displacement_facet(slot).equals(volume->texture_displacement_facet(slot));
if ((shared_volume->supported_facets.equals(volume->supported_facets))
&& (shared_volume->seam_facets.equals(volume->seam_facets))
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_facets))
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets)))
&& (shared_volume->fuzzy_skin_facets.equals(volume->fuzzy_skin_facets))
&& same_texture_paint)
{
auto data = iter->second.first;
const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
@@ -7651,6 +7764,20 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
output_buffer += "\"";
}
// One attribute per texture displacement layer slot. Older readers ignore attributes they do
// not know, so a project written here still opens in a build without the feature - it just
// loses the paint, which is also all it could have done with it.
for (int slot = 0; slot < TEXTURE_DISPLACEMENT_MAX_LAYERS; ++slot) {
const std::string texture_paint = volume->texture_displacement_facet(slot).get_triangle_as_string(i);
if (texture_paint.empty())
continue;
output_buffer += " ";
output_buffer += TEXTURE_DISPLACEMENT_ATTRS[slot];
output_buffer += "=\"";
output_buffer += texture_paint;
output_buffer += "\"";
}
// BBS
if (i < its.properties.size()) {
std::string prop_str = its.properties[i].to_string();
@@ -8100,7 +8227,42 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return file_path;
}
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it.
// The layer settings go in as JSON and each layer's texture as the image file it was loaded from, both
// referenced by path - the same split EmbossShape makes for its SVG. The image deliberately stays out of
// the XML: it is binary and routinely megabytes, and base64 in an attribute would bloat the one file
// every reader has to parse just to list the objects.
static void add_texture_displacement(std::stringstream &stream, const ModelVolume &volume, mz_zip_archive &archive,
const std::string &id)
{
if (volume.texture_displacement_layers.empty())
return;
// A copy, because path_in_3mf is only meaningful inside the archive being written and the volume
// being exported is const (and may be saved again, elsewhere, with different paths).
std::vector<TextureDisplacementLayer> layers = volume.texture_displacement_layers;
for (TextureDisplacementLayer &layer : layers) {
layer.path_in_3mf.clear();
if (!layer.image_data || layer.image_data->empty())
continue;
// Always .png: the importer converts whatever the user picked (jpg, bmp, ...) to an 8-bit
// greyscale PNG, so image_data is PNG whatever layer.path happens to be named.
const std::string path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + "_" + std::to_string(layer.slot) + ".png";
// No deflate: a PNG is already compressed, so a second pass only costs time.
if (mz_zip_writer_add_mem(&archive, path.c_str(), layer.image_data->data(), layer.image_data->size(),
MZ_NO_COMPRESSION))
layer.path_in_3mf = path;
}
const std::string json = texture_displacement_layers_to_json(layers, volume.texture_displacement_options);
const std::string json_path = std::string(TEXTURE_DISPLACEMENT_DIR) + id + ".json";
if (!mz_zip_writer_add_mem(&archive, json_path.c_str(), json.data(), json.size(), MZ_DEFAULT_COMPRESSION))
return;
stream << " <" << METADATA_TAG << " " << KEY_ATTR << "=\"" << TEXTURE_DISPLACEMENT_KEY << "\" "
<< VALUE_ATTR << "=\"" << xml_escape(json_path) << "\"/>\n";
}
bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archive, const Model& model, PlateDataPtrs& plate_data_list, const ObjectToObjectDataMap &objects_data, const DynamicPrintConfig& config, int export_plate_idx, bool save_gcode, bool use_loaded_id)
{
std::stringstream stream;
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
@@ -8211,6 +8373,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (const std::optional<EmbossShape> &es = volume->emboss_shape; es.has_value()) {
to_xml(stream, *es, *volume, archive, m_fullpath_sources);
}
add_texture_displacement(stream, *volume, archive, std::to_string(volume->id().id));
if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
tc.has_value())
-349
View File
@@ -1,349 +0,0 @@
#include "JumpPointSearch.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Point.hpp"
#include "libslic3r/AStar.hpp"
#include "libslic3r/KDTreeIndirect.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
#include <cstdlib>
#include <iterator>
#include <limits>
#include <optional>
#include <string>
#include <unordered_map>
#include <vector>
#include <oneapi/tbb/scalable_allocator.h>
//#define DEBUG_FILES
#ifdef DEBUG_FILES
#include "libslic3r/SVG.hpp"
#endif
namespace Slic3r {
// execute fn for each pixel on the line. If fn returns false, terminate the iteration
template<typename PointFn> void dda(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
coord_t dx = abs(x1 - x0);
coord_t dy = abs(y1 - y0);
coord_t x = x0;
coord_t y = y0;
coord_t n = 1 + dx + dy;
coord_t x_inc = (x1 > x0) ? 1 : -1;
coord_t y_inc = (y1 > y0) ? 1 : -1;
coord_t error = dx - dy;
dx *= 2;
dy *= 2;
for (; n > 0; --n) {
if (!fn(x, y)) return;
if (error > 0) {
x += x_inc;
error -= dy;
} else {
y += y_inc;
error += dx;
}
}
}
// will draw the line twice, second time with and offset of 1 in the direction of normal
// may call the fn on the same coordiantes multiple times!
template<typename PointFn> void double_dda_with_offset(coord_t x0, coord_t y0, coord_t x1, coord_t y1, const PointFn &fn)
{
Vec2d normal = Point{y1 - y0, x1 - x0}.cast<double>().normalized();
normal.x() = ceil(normal.x());
normal.y() = ceil(normal.y());
Point start_offset = Point(x0, y0) + (normal).cast<coord_t>();
Point end_offset = Point(x1, y1) + (normal).cast<coord_t>();
dda(x0, y0, x1, y1, fn);
dda(start_offset.x(), start_offset.y(), end_offset.x(), end_offset.y(), fn);
}
template<typename CellPositionType, typename CellQueryFn> class JPSTracer
{
public:
// Use incoming_dir [0,0] for starting points, so that all directions are checked from that point
struct Node
{
CellPositionType position;
CellPositionType incoming_dir;
};
JPSTracer(CellPositionType target, CellQueryFn is_passable) : target(target), is_passable(is_passable) {}
private:
CellPositionType target;
CellQueryFn is_passable; // should return boolean whether the cell is passable or not
CellPositionType find_jump_point(CellPositionType start, CellPositionType forward_dir) const
{
CellPositionType next = start + forward_dir;
while (next != target && is_passable(next) && !(is_jump_point(next, forward_dir))) { next = next + forward_dir; }
if (is_passable(next)) {
return next;
} else {
return start;
}
}
bool is_jump_point(CellPositionType pos, CellPositionType forward_dir) const
{
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) { return true; }
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) { return true; }
if (find_jump_point(pos, horizontal_check_dir) != pos) { return true; }
if (find_jump_point(pos, vertical_check_dir) != pos) { return true; }
return false;
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { return true; }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { return true; }
return false;
}
}
public:
template<class Fn> void foreach_reachable(const Node &from, Fn &&fn) const
{
const CellPositionType & pos = from.position;
const CellPositionType & forward_dir = from.incoming_dir;
std::vector<CellPositionType> dirs_to_check{};
if (abs(forward_dir.x()) + abs(forward_dir.y()) == 0) { // special case for starting point
dirs_to_check = all_directions;
} else if (abs(forward_dir.x()) + abs(forward_dir.y()) == 2) {
// diagonal
CellPositionType horizontal_check_dir = CellPositionType{forward_dir.x(), 0};
CellPositionType vertical_check_dir = CellPositionType{0, forward_dir.y()};
if (!is_passable(pos - horizontal_check_dir) && is_passable(pos + forward_dir - 2 * horizontal_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * horizontal_check_dir);
}
if (!is_passable(pos - vertical_check_dir) && is_passable(pos + forward_dir - 2 * vertical_check_dir)) {
dirs_to_check.push_back(forward_dir - 2 * vertical_check_dir);
}
dirs_to_check.push_back(horizontal_check_dir);
dirs_to_check.push_back(vertical_check_dir);
dirs_to_check.push_back(forward_dir);
} else { // horizontal or vertical
CellPositionType side_dir = CellPositionType(forward_dir.y(), forward_dir.x());
if (!is_passable(pos + side_dir) && is_passable(pos + forward_dir + side_dir)) { dirs_to_check.push_back(forward_dir + side_dir); }
if (!is_passable(pos - side_dir) && is_passable(pos + forward_dir - side_dir)) { dirs_to_check.push_back(forward_dir - side_dir); }
dirs_to_check.push_back(forward_dir);
}
for (const CellPositionType &dir : dirs_to_check) {
CellPositionType jp = find_jump_point(pos, dir);
if (jp != pos) fn(Node{jp, dir});
}
}
float distance(Node a, Node b) const { return (a.position - b.position).template cast<double>().norm(); }
float goal_heuristic(Node n) const { return n.position == target ? -1.f : (target - n.position).template cast<double>().norm(); }
size_t unique_id(Node n) const { return (static_cast<size_t>(uint16_t(n.position.x())) << 16) + static_cast<size_t>(uint16_t(n.position.y())); }
const std::vector<CellPositionType> all_directions{{1, 0}, {1, 1}, {0, 1}, {-1, 1}, {-1, 0}, {-1, -1}, {0, -1}, {1, -1}};
};
void JPSPathFinder::clear()
{
inpassable.clear();
max_search_box.max = Pixel(std::numeric_limits<coord_t>::min(), std::numeric_limits<coord_t>::min());
max_search_box.min = Pixel(std::numeric_limits<coord_t>::max(), std::numeric_limits<coord_t>::max());
add_obstacles(bed_shape);
}
void JPSPathFinder::add_obstacles(const Lines &obstacles)
{
auto store_obstacle = [&](coord_t x, coord_t y) {
max_search_box.max.x() = std::max(max_search_box.max.x(), x);
max_search_box.max.y() = std::max(max_search_box.max.y(), y);
max_search_box.min.x() = std::min(max_search_box.min.x(), x);
max_search_box.min.y() = std::min(max_search_box.min.y(), y);
inpassable.insert(Pixel{x, y});
return true;
};
for (const Line &l : obstacles) {
Pixel start = pixelize(l.a);
Pixel end = pixelize(l.b);
double_dda_with_offset(start.x(), start.y(), end.x(), end.y(), store_obstacle);
}
}
Polyline JPSPathFinder::find_path(const Point &p0, const Point &p1)
{
Pixel start = pixelize(p0);
Pixel end = pixelize(p1);
if (inpassable.empty() || (start - end).cast<float>().norm() < 3.0) { return Polyline{p0, p1}; }
if (inpassable.find(start) != inpassable.end()) {
dda(start.x(), start.y(), end.x(), end.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
start = Pixel(x, y);
return false;
}
return true;
});
}
if (inpassable.find(end) != inpassable.end()) {
dda(end.x(), end.y(), start.x(), start.y(), [&](coord_t x, coord_t y) {
if (inpassable.find(Pixel(x, y)) == inpassable.end() || start == end) { // new start not found yet, and xy passable
end = Pixel(x, y);
return false;
}
return true;
});
}
BoundingBox search_box = max_search_box;
search_box.max -= Pixel(1, 1);
search_box.min += Pixel(1, 1);
BoundingBox bounding_square(Points{start, end});
bounding_square.max += Pixel(5, 5);
bounding_square.min -= Pixel(5, 5);
coord_t bounding_square_size = 2 * std::max(bounding_square.size().x(), bounding_square.size().y());
bounding_square.max.x() += (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.min.x() -= (bounding_square_size - bounding_square.size().x()) / 2;
bounding_square.max.y() += (bounding_square_size - bounding_square.size().y()) / 2;
bounding_square.min.y() -= (bounding_square_size - bounding_square.size().y()) / 2;
// Intersection - limit the search box to a square area around the start and end, to fasten the path searching
search_box.max = search_box.max.cwiseMin(bounding_square.max);
search_box.min = search_box.min.cwiseMax(bounding_square.min);
auto cell_query = [&](Pixel pixel) { return search_box.contains(pixel) && (pixel == start || pixel == end || inpassable.find(pixel) == inpassable.end()); };
JPSTracer<Pixel, decltype(cell_query)> tracer(end, cell_query);
using QNode = astar::QNode<JPSTracer<Pixel, decltype(cell_query)>>;
std::unordered_map<size_t, QNode> astar_cache{};
std::vector<Pixel, PointsAllocator<Pixel>> out_path;
std::vector<decltype(tracer)::Node> out_nodes;
if (!astar::search_route(tracer, {start, {0, 0}}, std::back_inserter(out_nodes), astar_cache)) {
// path not found - just reconstruct the best path from astar cache.
// Note that astar_cache is NOT empty - at least the starting point should always be there
auto coordiante_func = [&astar_cache](size_t idx, size_t dim) { return float(astar_cache[idx].node.position[dim]); };
std::vector<size_t> keys;
keys.reserve(astar_cache.size());
for (const auto &pair : astar_cache) { keys.push_back(pair.first); }
KDTreeIndirect<2, float, decltype(coordiante_func)> kd_tree(coordiante_func, keys);
size_t closest_qnode = find_closest_point(kd_tree, end.cast<float>());
out_path.push_back(end);
while (closest_qnode != astar::Unassigned) {
out_path.push_back(astar_cache[closest_qnode].node.position);
closest_qnode = astar_cache[closest_qnode].parent;
}
} else {
for (const auto &node : out_nodes) { out_path.push_back(node.position); }
out_path.push_back(start);
}
#ifdef DEBUG_FILES
auto scaled_points = [](const Points &ps) {
Points r;
for (const Point &p : ps) { r.push_back(Point::new_scale(p.x(), p.y())); }
return r;
};
auto scaled_point = [](const Point &p) { return Point::new_scale(p.x(), p.y()); };
::Slic3r::SVG svg(debug_out_path(("path_jps" + std::to_string(print_z) + "_" + std::to_string(rand() % 1000)).c_str()).c_str(),
BoundingBox(scaled_point(search_box.min), scaled_point(search_box.max)));
for (const auto &p : inpassable) { svg.draw(scaled_point(p), "black", scale_(0.4)); }
for (const auto &qn : astar_cache) { svg.draw(scaled_point(qn.second.node.position), "blue", scale_(0.3)); }
svg.draw(Polyline(scaled_points(out_path)), "yellow", scale_(0.25));
svg.draw(scaled_point(end), "purple", scale_(0.4));
svg.draw(scaled_point(start), "green", scale_(0.4));
#endif
std::vector<Pixel, PointsAllocator<Pixel>> tmp_path;
tmp_path.reserve(out_path.size());
// Some path found, reverse and remove points that do not change direction
std::reverse(out_path.begin(), out_path.end());
{
tmp_path.push_back(out_path.front()); // first point
for (size_t i = 1; i < out_path.size() - 1; i++) {
if ((out_path[i] - out_path[i - 1]).cast<float>().normalized() != (out_path[i + 1] - out_path[i]).cast<float>().normalized()) { tmp_path.push_back(out_path[i]); }
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "orange", scale_(0.20));
#endif
tmp_path.clear();
// remove redundant jump points - there are points that change direction but are not needed - this inefficiency arises from the
// usage of grid search The removal alg tries to find the longest Px Px+k path without obstacles. If Px Px+k+1 is blocked, it will
// insert the Px+k point to result and continue search from Px+k
{
tmp_path.push_back(out_path.front()); // first point
size_t index_of_last_stored_point = 0;
for (size_t i = 1; i < out_path.size(); i++) {
if (i - index_of_last_stored_point < 2) continue;
bool passable = true;
auto store_obstacle = [&](coord_t x, coord_t y) {
if (Pixel(x, y) != start && Pixel(x, y) != end && inpassable.find(Pixel(x, y)) != inpassable.end()) {
passable = false;
return false;
}
return true;
};
dda(tmp_path.back().x(), tmp_path.back().y(), out_path[i].x(), out_path[i].y(), store_obstacle);
if (!passable) {
tmp_path.push_back(out_path[i - 1]);
index_of_last_stored_point = i - 1;
}
}
tmp_path.push_back(out_path.back()); // last_point
out_path = tmp_path;
}
#ifdef DEBUG_FILES
svg.draw(Polyline(scaled_points(out_path)), "red", scale_(0.15));
svg.Close();
#endif
// before returing the path, transform it from pixels back to points.
// Also replace the first and last pixel by input points so that result path patches input params exactly.
for (Pixel &p : out_path) { p = unpixelize(p); }
out_path.front() = p0;
out_path.back() = p1;
return Polyline(out_path);
}
} // namespace Slic3r
-39
View File
@@ -1,39 +0,0 @@
#pragma once
#include "Line.hpp"
#ifndef SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#define SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_
#include "BoundingBox.hpp"
#include "Polygon.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/libslic3r.h"
#include <unordered_map>
#include <unordered_set>
namespace Slic3r {
class JPSPathFinder
{
using Pixel = Point;
std::unordered_set<Pixel, PointHash> inpassable;
coordf_t print_z;
BoundingBox max_search_box;
Lines bed_shape;
const coord_t resolution = scaled(1.5);
Pixel pixelize(const Point &p) { return p / resolution; }
Point unpixelize(const Pixel &p) { return p * resolution; }
public:
JPSPathFinder() = default;
void init_bed_shape(const Points &bed_shape) { this->bed_shape = (to_lines(Polygon{bed_shape})); };
void clear();
void add_obstacles(const Lines &obstacles);
Polyline find_path(const Point &start, const Point &end);
};
} // namespace Slic3r
#endif /* SRC_LIBSLIC3R_JUMPPOINTSEARCH_HPP_ */
+22
View File
@@ -353,6 +353,24 @@ Model Model::read_from_file(const std::string&
ObjInfo obj_info;
ObjParser::MtlData mtl_data;
result = load_obj(input_file.c_str(), &model, obj_info, message, nullptr, &mtl_data);
// Keep the model's own UV channel on the volume, whatever the colour import then does with
// the mesh: it is what TextureProjectionMethod::ModelUV maps a texture-displacement layer
// through, so a model that arrives with a UV layout is textured on that layout instead of on
// a fresh unwrap. load_obj() fills obj_info.uvs per face of the mesh it built, in the same
// order (and only when every face named a known material, hence the size check). V is flipped
// on the way in, to the convention DecodedHeightTexture samples in - the same flip
// obj_to_textured_mesh() does for the colour importer.
if (result && !obj_info.uvs.empty() && !model.objects.empty() && !model.objects.back()->volumes.empty()) {
ModelVolume &volume = *model.objects.back()->volumes[0];
const indexed_triangle_set &its = volume.mesh().its;
if (obj_info.uvs.size() == its.indices.size()) {
volume.texture_uv.resize(its.indices.size() * 3);
for (size_t fi = 0; fi < obj_info.uvs.size(); ++fi)
for (int k = 0; k < 3; ++k)
volume.texture_uv[fi * 3 + size_t(k)] =
Vec2f(obj_info.uvs[fi][size_t(k)].x(), 1.f - obj_info.uvs[fi][size_t(k)].y());
}
}
if (result && obj_info.has_uv_png && !obj_info.uvs.empty() && !model.objects.empty()) {
// Textured OBJ: hand the mesh + materials to the texture-to-color importer
// instead of the flat per-face colour dialog.
@@ -2133,6 +2151,10 @@ void ModelVolume::reset_extra_facets()
// 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();
// Same reasoning for the model's own UV channel: it is indexed by triangle, so against a mesh
// this no longer matches it would map the texture by another mesh's triangle numbering. A caller
// that can carry it across - see transfer_uv_channel() - puts it back after this.
this->reset_texture_uv();
}
std::optional<TriangleSelector::SavedPainting> ModelVolume::save_painting() const
+23 -2
View File
@@ -974,6 +974,26 @@ public:
// they are not a property of any single layer.
TextureDisplacementOptions texture_displacement_options;
// The model's own texture coordinates, as the file it was imported from carried them: three per
// triangle of mesh().its, in its triangle order (corner k of triangle i at 3i+k), V already
// flipped to the sampling convention (V = 0 is the image's top row). Empty when the source had
// none, which is every STL and every 3mf - only a textured OBJ brings one in today.
//
// Per corner rather than per vertex because that is what a UV layout is: the two sides of a seam
// are the same vertex with two different coordinates, and collapsing them would smear the texture
// across every island boundary. TextureProjectionMethod::ModelUV maps through it, which is what
// lets a model that already carries a UV channel be textured on the artist's own layout instead
// of on a fresh unwrap.
//
// Indexed by triangle, so anything that *replaces* the mesh has to bring it across (or drop it):
// see transfer_uv_channel(), and reset_texture_uv() for the cases that cannot.
std::vector<Vec2f> texture_uv;
bool has_texture_uv() const { return !texture_uv.empty() && texture_uv.size() == mesh().its.indices.size() * 3; }
// Forgets the channel. For a mesh change that nothing can carry it across - a split, a boolean -
// where leaving it would map the texture by another mesh's triangle numbering.
void reset_texture_uv() { texture_uv.clear(); }
// Save painting data before reset_extra_facets() discards it.
// Used for replacing mesh without losing painting data.
// Only for model parts (not modifiers/connectors).
@@ -1236,6 +1256,7 @@ private:
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),
texture_uv(other.texture_uv),
cut_info(other.cut_info), text_configuration(other.text_configuration), emboss_shape(other.emboss_shape)
{
assert(this->id().valid());
@@ -1334,7 +1355,7 @@ private:
cereal::load_by_value(ar, f);
mesh_changed |= tf != f.timestamp();
}
ar(texture_displacement_layers, texture_displacement_options);
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
cereal::load_by_value(ar, config);
cereal::load(ar, text_configuration);
cereal::load(ar, emboss_shape);
@@ -1358,7 +1379,7 @@ private:
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);
ar(texture_displacement_layers, texture_displacement_options, texture_uv);
cereal::save_by_value(ar, config);
cereal::save(ar, text_configuration);
cereal::save(ar, emboss_shape);
+522 -59
View File
@@ -19,6 +19,7 @@
#include <mutex>
#include <numeric>
#include <optional>
#include "nlohmann/json.hpp"
#include <queue>
#include <ratio>
#include <string>
@@ -1570,6 +1571,61 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
return islands;
}
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh)
{
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_paint.bitstream.empty())
return {};
const TriangleMesh src(src_mesh);
TriangleSelector selector(src);
selector.deserialize(src_paint, false);
// Every sub-triangle of the source, each carrying its own state - NONE included, so the soup
// covers the whole surface and a foot point always lands on something. One soup rather than one
// tree per state: a foot point has one nearest triangle, and that triangle's state is the answer.
std::vector<indexed_triangle_set> per_state;
selector.get_facets(per_state);
indexed_triangle_set soup;
std::vector<uint8_t> label;
for (size_t st = 0; st < per_state.size(); ++st) {
const indexed_triangle_set &part = per_state[st];
if (part.indices.empty())
continue;
const int base = int(soup.vertices.size());
soup.vertices.insert(soup.vertices.end(), part.vertices.begin(), part.vertices.end());
for (const stl_triangle_vertex_indices &t : part.indices) {
soup.indices.emplace_back(t[0] + base, t[1] + base, t[2] + base);
label.push_back(uint8_t(st));
}
}
if (soup.indices.empty())
return {};
const AABBTreeIndirect::Tree3f tree =
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(soup.vertices, soup.indices);
std::vector<uint8_t> out(dst_mesh.indices.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t j = range.begin(); j < range.end(); ++j) {
const stl_triangle_vertex_indices &t = dst_mesh.indices[j];
// The centroid, not a corner: a facet takes one state, and its middle is the point that
// is inside it whatever the triangle looks like.
const Vec3f centroid = (dst_mesh.vertices[size_t(t[0])] + dst_mesh.vertices[size_t(t[1])] +
dst_mesh.vertices[size_t(t[2])]) / 3.f;
size_t hit = 0;
Vec3f foot;
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(soup.vertices, soup.indices,
tree, centroid, hit, foot);
if (d2 >= 0.f && hit < label.size())
out[j] = label[hit];
}
});
return out;
}
void average_island_scales(std::vector<TextureIsland> &islands)
{
if (islands.empty())
@@ -1672,6 +1728,110 @@ std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, co
return corner;
}
// Barycentric weights of `p` inside triangle (a, b, c), as the pair that belongs with the edges
// (b - a) and (c - a): p ~= (1 - w1 - w2) * a + w1 * b + w2 * c. `p` is projected onto the triangle's
// plane, so a point a hair off the surface still reports the weights of its foot point. A degenerate
// triangle reports its centroid, the only coordinate that means anything there.
static void barycentric_weights(const Vec3f &a, const Vec3f &b, const Vec3f &c, const Vec3f &p, float &w1, float &w2)
{
const Vec3f e0 = b - a, e1 = c - a, ep = p - a;
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
const float den = d00 * d11 - d01 * d01;
if (std::abs(den) <= 1e-20f) {
w1 = w2 = 1.f / 3.f;
return;
}
w1 = (d11 * dp0 - d01 * dp1) / den;
w2 = (d00 * dp1 - d01 * dp0) / den;
}
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
const std::vector<Vec2f> &model_uv,
const indexed_triangle_set &patch,
const std::vector<int> &patch_source)
{
if (patch.indices.empty() || model_uv.size() != base_mesh.indices.size() * 3 ||
patch_source.size() != patch.indices.size())
return {};
std::vector<Vec2f> corner(patch.indices.size() * 3, Vec2f::Zero());
for (size_t j = 0; j < patch.indices.size(); ++j) {
const int S = patch_source[j];
if (S < 0 || size_t(S) >= base_mesh.indices.size())
continue; // no source triangle: leave (0, 0), which the callers read as "no placement"
const stl_triangle_vertex_indices &src = base_mesh.indices[size_t(S)];
const Vec3f &a = base_mesh.vertices[size_t(src[0])];
const Vec3f &b = base_mesh.vertices[size_t(src[1])];
const Vec3f &c = base_mesh.vertices[size_t(src[2])];
const Vec2f *uv = &model_uv[size_t(S) * 3];
const stl_triangle_vertex_indices &tri = patch.indices[j];
for (int k = 0; k < 3; ++k) {
// An unsplit patch triangle is its source triangle, so its corners land exactly on a, b
// and c and this reproduces their uvs unchanged; a sub-triangle the brush cut gets the
// interpolated coordinate, which is the whole point of going through the barycentric.
float w1 = 0.f, w2 = 0.f;
barycentric_weights(a, b, c, patch.vertices[size_t(tri[k])], w1, w2);
corner[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
}
}
return corner;
}
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner)
{
if (patch.indices.empty() || corner.size() != patch.indices.size() * 3)
return {};
std::vector<Vec2f> per_vertex(patch.vertices.size(), Vec2f::Zero());
std::vector<char> seen(patch.vertices.size(), 0);
for (size_t j = 0; j < patch.indices.size(); ++j)
for (int k = 0; k < 3; ++k) {
const int vi = patch.indices[j][k];
if (vi < 0 || size_t(vi) >= per_vertex.size() || seen[size_t(vi)])
continue;
seen[size_t(vi)] = 1;
per_vertex[size_t(vi)] = corner[j * 3 + size_t(k)];
}
return per_vertex;
}
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
const std::vector<Vec2f> &src_uv,
const indexed_triangle_set &dst_mesh,
float max_distance)
{
if (src_mesh.indices.empty() || dst_mesh.indices.empty() || src_uv.size() != src_mesh.indices.size() * 3)
return {};
const AABBTreeIndirect::Tree3f tree =
AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(src_mesh.vertices, src_mesh.indices);
const float limit2 = max_distance >= 0.f ? max_distance * max_distance : std::numeric_limits<float>::max();
std::vector<Vec2f> out(dst_mesh.indices.size() * 3, Vec2f::Zero());
tbb::parallel_for(tbb::blocked_range<size_t>(0, dst_mesh.indices.size()),
[&](const tbb::blocked_range<size_t> &range) {
for (size_t j = range.begin(); j < range.end(); ++j) {
const stl_triangle_vertex_indices &tri = dst_mesh.indices[j];
for (int k = 0; k < 3; ++k) {
const Vec3f &p = dst_mesh.vertices[size_t(tri[k])];
size_t hit = 0;
Vec3f foot;
const float d2 = AABBTreeIndirect::squared_distance_to_indexed_triangle_set(
src_mesh.vertices, src_mesh.indices, tree, p, hit, foot);
if (d2 < 0.f || d2 > limit2 || hit >= src_mesh.indices.size())
continue; // nowhere near the source surface: (0, 0), as documented
const stl_triangle_vertex_indices &st = src_mesh.indices[hit];
float w1 = 0.f, w2 = 0.f;
barycentric_weights(src_mesh.vertices[size_t(st[0])], src_mesh.vertices[size_t(st[1])],
src_mesh.vertices[size_t(st[2])], foot, w1, w2);
const Vec2f *uv = &src_uv[hit * 3];
out[j * 3 + size_t(k)] = (1.f - w1 - w2) * uv[0] + w1 * uv[1] + w2 * uv[2];
}
}
});
return out;
}
namespace {
// apply_uv_transform()'s per-layer constants, worked out once. Triplanar sampling runs the transform
// three times per point, and recomputing the rotation's cos/sin and the tiling reciprocal on every one
@@ -1681,13 +1841,16 @@ struct UVTransform
float scale, cs, sn, aspect;
Vec2f offset;
UVTransform(const TextureDisplacementLayer &layer, float aspect_)
// `normalized_uv` drops the two steps that only mean something for a coordinate measured on the
// model - the millimetre tiling scale and the non-square aspect correction - and keeps the
// rotation and offset. See apply_uv_transform(); TextureProjectionMethod::ModelUV is what needs it.
UVTransform(const TextureDisplacementLayer &layer, float aspect_, bool normalized_uv = false)
{
scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
cs = std::cos(rad);
sn = std::sin(rad);
aspect = aspect_;
aspect = normalized_uv ? 1.f : aspect_;
offset = layer.offset;
}
Vec2f operator()(const Vec2f &planar) const
@@ -1712,9 +1875,11 @@ inline Vec3f triplanar_weights(const Vec3f &normal)
}
} // namespace
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect)
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect, bool normalized_uv)
{
const float scale = (layer.tiling_scale > 1e-6f) ? (1.f / layer.tiling_scale) : 1.f;
// A uv that is already normalized has no millimetre scale to divide by and no aspect to correct -
// see the header. UVTransform does the same, and the two must stay in step.
const float scale = (normalized_uv || layer.tiling_scale <= 1e-6f) ? 1.f : (1.f / layer.tiling_scale);
const Vec2f scaled = planar * scale;
const float rad = layer.rotation_deg * float(M_PI) / 180.f;
@@ -1732,7 +1897,7 @@ Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &la
// Applied after the rotation, not before: scaling one axis of an already-rotated coordinate is a
// shear, and doing it the other way round would make "Rotation" skew the pattern instead of
// turning it.
if (aspect > 0.f && aspect != 1.f)
if (!normalized_uv && aspect > 0.f && aspect != 1.f)
rotated.y() *= aspect;
return rotated + layer.offset;
@@ -1757,6 +1922,9 @@ float blend_displacement(float accumulated, float value, TextureBlendMode mode)
value;
return accumulated / divisor;
}
// Replaces instead of combining, so `accumulated` is deliberately unused: whatever the layers
// below put here is gone where this one is painted.
case TextureBlendMode::Override: return value;
case TextureBlendMode::Add:
default: return accumulated + value;
}
@@ -1786,14 +1954,21 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
// apply_uv_transform()). Every projection except the projective "from view" one funnels through
// here, so this one line is what makes them all aspect-correct.
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
const UVTransform xf(layer, aspect);
// A ModelUV layer's precomputed uv is the model's own, already normalized; every other coordinate
// reaching the transform - the unwrap's, and every analytic projection's - is in millimetres. The
// `lscm_uv` test is what keeps the two apart: a ModelUV layer with no channel gets none, falls
// through to the millimetre methods below, and must be scaled like them.
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
lscm_uv != nullptr;
const UVTransform xf(layer, aspect, normalized_uv);
auto sample_at = [&](const Vec2f &planar) {
return texture.sample(xf(planar), layer.tile_enabled, layer.tile_method);
};
// Precomputed per-patch LSCM solve wins over the layer's own method (see the header): the
// caller only passes it when the patch actually parameterized successfully, so a patch that
// failed to unwrap falls through to the analytic methods below as its documented fallback.
// A precomputed per-patch uv - the LSCM solve, or the model's own channel - wins over the layer's
// own method (see the header): the caller only passes it when the patch actually has one, so a
// patch that failed to unwrap, or a volume with no UV channel, falls through to the analytic
// methods below as its documented fallback.
if (lscm_uv != nullptr)
return sample_at(*lscm_uv);
@@ -1851,8 +2026,12 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
// the two differ in what "nothing here" means. Height returns 0, which is a perfectly good height
// (no displacement); colour has no such neutral value - black is a colour - so every path that
// returns 0 there has to report false here instead, and the caller leaves the triangle uncoloured.
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
const UVTransform xf(layer, aspect);
const float aspect = (texture.height > 0) ? float(texture.width) / float(texture.height) : 1.f;
// Same reasoning as sample_layer_height(), and it has to match it exactly or a layer's colour and
// its relief would be read from different places in the image.
const bool normalized_uv = layer.projection_method == TextureProjectionMethod::ModelUV &&
lscm_uv != nullptr;
const UVTransform xf(layer, aspect, normalized_uv);
auto sample_at = [&](const Vec2f &planar) {
return texture.sample_color(xf(planar), layer.tile_enabled, layer.tile_method);
};
@@ -1866,6 +2045,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
};
if (lscm_uv != nullptr) {
// The coverage test goes through the same transform, so an untiled ModelUV layer is "there"
// exactly where the model's own uv lands inside the image.
if (!covered(*lscm_uv))
return false;
out = sample_at(*lscm_uv);
@@ -2210,9 +2391,10 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
const TextureColorRequest *color,
bool flip_normals,
BakeStageRecorder *debug,
TextureBakeStats *stats)
TextureBakeStats *stats,
const std::vector<Vec2f> *model_uv)
{
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data);
HeightFieldSampler combined = make_combined_displacement_sampler(mesh, layers, facets_data, model_uv);
if (!combined)
return mesh; // nothing decodable to displace with
@@ -2382,7 +2564,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
// part of it (half a tile in the neighbour's colour, a tile edge that wanders).
const bool want_color = color != nullptr && color->out_triangle != nullptr && bool(color->quantize);
const ColorFieldSampler color_sampler =
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure) : ColorFieldSampler{};
want_color ? make_combined_color_sampler(mesh, layers, facets_data, color->quantize, color->quantize_pure, model_uv) :
ColorFieldSampler{};
//
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
@@ -2514,7 +2697,8 @@ static indexed_triangle_set build_texture_displacement_in_place(
const TextureColorRequest *color,
bool flip_normals,
BakeStageRecorder *debug,
TextureBakeStats *stats)
TextureBakeStats *stats,
const std::vector<Vec2f> *model_uv)
{
// The classic path moves the vertices the mesh already has, so there is no budget to report on.
(void) stats;
@@ -2551,7 +2735,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
if (options.pipeline_v2)
return build_texture_displacement_v2(mesh, layers, facets_data, options, progress, color, flip_normals,
debug, stats);
debug, stats, model_uv);
// Layers are combined in slot order, like stacked layers in an image editor: each one folds its
// own displacement into the running total via its blend mode (see TextureBlendMode).
@@ -2669,13 +2853,19 @@ static indexed_triangle_set build_texture_displacement_in_place(
selector_dirty = true;
const bool color_this_layer = want_color && layer->color_enabled;
// The model's own channel is per base-mesh *triangle*, so reading it needs to know which base
// triangle each (possibly brush-split) patch triangle came from - the colour pass's map, asked
// for here as well.
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
model_uv != nullptr && model_uv->size() == mesh.indices.size() * 3;
// A flat-colour image is matched against the filaments alone (see TextureColorRequest).
const ColorQuantizeFn &layer_quantize =
(color_this_layer && color->quantize_pure && analyze_texture_detail(*layer).flat_colors) ? color->quantize_pure
: color->quantize;
std::vector<int> patch_source; // sub-triangle -> base mesh triangle, only built when colouring
std::vector<int> patch_source; // sub-triangle -> base mesh triangle; built when colouring or mapping by ModelUV
const indexed_triangle_set patch =
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, color_this_layer ? &patch_source : nullptr);
selector.get_facets_strict(EnforcerBlockerType::ENFORCER,
(color_this_layer || model_uv_layer) ? &patch_source : nullptr);
if (patch.indices.empty())
continue;
// get_facets_strict() returns the same vertex array whichever state is asked for (only the
@@ -2706,18 +2896,25 @@ static indexed_triangle_set build_texture_displacement_in_place(
// A real unwrap of the whole patch, computed once here rather than per vertex - it is a
// per-chart solve over the whole patch, not a per-point formula. Cached, so repeating this
// for every slider tweak costs a hash rather than a re-solve (see compute_patch_unwrap()).
const std::vector<Vec2f> lscm_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
// ModelUV takes its corners from the volume's own channel instead of solving anything, and
// collapses them to one per vertex the same way the unwrap does.
const std::vector<Vec2f> model_corner_uvs = model_uv_layer ?
compute_model_corner_uvs(mesh, *model_uv, patch, patch_source) :
std::vector<Vec2f>{};
const std::vector<Vec2f> patch_uvs = model_uv_layer ? collapse_corner_uvs(patch, model_corner_uvs) :
(layer->projection_method == TextureProjectionMethod::LSCM) ?
compute_lscm_uvs(patch, *layer) :
std::vector<Vec2f>{};
// The colour pass below samples per *triangle*, so it takes the per-corner unwrap instead: the
// per-vertex collapse above would hand a triangle at a seam the island layout did not join its
// neighbour's placement, painting one triangle per face from the wrong part of the texture.
// (The displacement itself stays on lscm_uvs - a vertex has one position, so one height.)
const std::vector<Vec2f> lscm_corner_uvs = (layer->projection_method == TextureProjectionMethod::LSCM) ?
// (The displacement itself stays on patch_uvs - a vertex has one position, so one height.)
const std::vector<Vec2f> patch_corner_uvs = model_uv_layer ? model_corner_uvs :
(layer->projection_method == TextureProjectionMethod::LSCM) ?
compute_lscm_corner_uvs(patch, *layer) :
std::vector<Vec2f>{};
const bool corner_uv_ok = !lscm_corner_uvs.empty() &&
lscm_corner_uvs.size() == patch.indices.size() * 3;
const bool corner_uv_ok = !patch_corner_uvs.empty() &&
patch_corner_uvs.size() == patch.indices.size() * 3;
// Colour, if this layer carries any. Area-weighted over each base triangle's *painted* part,
// so a triangle the brush only clipped a corner off takes the colour of that corner rather
@@ -2747,7 +2944,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
// the per-vertex height sampling uses, evaluated at the centroid instead.
Vec3f n = Vec3f::Zero();
Vec2f uv = Vec2f::Zero();
bool have_uv = !lscm_uvs.empty();
bool have_uv = !patch_uvs.empty();
for (int k = 0; k < 3; ++k) {
const int vi = t[k];
if (vi < int(vertex_normals.size()))
@@ -2755,9 +2952,9 @@ static indexed_triangle_set build_texture_displacement_in_place(
if (!have_uv)
continue;
if (corner_uv_ok)
uv += lscm_corner_uvs[j * 3 + size_t(k)];
else if (size_t(vi) < lscm_uvs.size())
uv += lscm_uvs[size_t(vi)];
uv += patch_corner_uvs[j * 3 + size_t(k)];
else if (size_t(vi) < patch_uvs.size())
uv += patch_uvs[size_t(vi)];
else
have_uv = false;
}
@@ -2831,7 +3028,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
[&](const tbb::blocked_range<size_t> &range) {
for (size_t k = range.begin(); k < range.end(); ++k) {
const size_t vi = size_t(layer_vertices[k]);
const Vec2f *lscm_uv = lscm_uvs.empty() ? nullptr : &lscm_uvs[vi];
const Vec2f *lscm_uv = patch_uvs.empty() ? nullptr : &patch_uvs[vi];
const float h = sample_layer_height(height, *layer, mesh.vertices[vi], vertex_normals[vi],
patch_centroid, patch_axis, lscm_uv);
// midlevel is the height that means "stay put", so anything below it displaces
@@ -2964,7 +3161,8 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
const TextureColorRequest *color,
const Transform3d &volume_to_world,
BakeStageRecorder *debug,
TextureBakeStats *stats)
TextureBakeStats *stats,
const std::vector<Vec2f> *model_uv)
{
// An untransformed volume on an untransformed instance is by far the common case, and the round
// trip costs two matrix multiplies per vertex on a mesh that can carry millions of them - so take
@@ -2974,7 +3172,7 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
const Transform3d frame = texture_displacement_bake_frame(volume_to_world);
if (frame.matrix().isApprox(Transform3d::Identity().matrix()))
return build_texture_displacement_in_place(base_mesh, layers, facets_data, options, progress, color,
false, debug, stats);
false, debug, stats, model_uv);
const Transform3d to_local = frame.inverse();
// A mirroring placement leaves the positions correct but every winding-derived normal pointing
@@ -2988,8 +3186,10 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
v = (frame * v.cast<double>()).cast<float>();
const size_t debug_mark = (debug != nullptr) ? debug->mark() : 0;
// The UV channel rides along untouched: a uv is a coordinate in the texture, not on the model, so
// the frame the mesh is measured in does not reach it.
indexed_triangle_set out = build_texture_displacement_in_place(world, layers, facets_data, options,
progress, color, mirrored, debug, stats);
progress, color, mirrored, debug, stats, model_uv);
// Everything the bake recorded is in world millimetres, like `out` itself. The debug view draws in
// the volume's local frame, so the stages are brought back the same way the result is.
if (debug != nullptr)
@@ -3061,7 +3261,8 @@ indexed_triangle_set build_texture_displacement(const ModelVolume &volume)
return build_texture_displacement(volume.mesh().its, volume.texture_displacement_layers, facets_data,
volume.texture_displacement_options, {}, nullptr,
texture_displacement_volume_to_world(volume));
texture_displacement_volume_to_world(volume), nullptr, nullptr,
volume.has_texture_uv() ? &volume.texture_uv : nullptr);
}
void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t> &movable, float strength,
@@ -3125,16 +3326,18 @@ void smooth_mesh_vertices(indexed_triangle_set &mesh, const std::vector<uint8_t>
namespace {
// One decoded texture + placement per sampleable layer, in blend (slot) order. Held by shared_ptr so
// the returned closure owns it for as long as the subdivider keeps calling back.
// An unwrap turned into something a *point* sampler can use. LSCM has no formula from position to
// uv - it is a per-triangle map - so a point is placed on the painted patch (the nearest patch
// triangle, and its barycentric coordinates there) and the uv is interpolated from that triangle's own
// per-corner uvs. Exact for a point on the base surface, which is where both samplers are queried: the
// displacement samples refined positions before moving them, and the colour pass samples the foot
// point on the painted pieces.
// A per-triangle uv map turned into something a *point* sampler can use. Neither of the two maps that
// are not a formula from position to uv - the LSCM unwrap and the model's own UV channel - can be
// evaluated at an arbitrary point directly, so a point is placed on the painted patch (the nearest
// patch triangle, and its barycentric coordinates there) and the uv is interpolated from that
// triangle's own per-corner uvs. Exact for a point on the base surface, which is where both samplers
// are queried: the displacement samples refined positions before moving them, and the colour pass
// samples the foot point on the painted pieces.
struct LscmLookup {
indexed_triangle_set patch; // the layer's painted patch, as the unwrap was solved on
indexed_triangle_set patch; // the layer's painted patch, as the uvs were computed on
AABBTreeIndirect::Tree3f tree;
std::vector<Vec2f> corner; // compute_lscm_corner_uvs(patch, layer)
// compute_lscm_corner_uvs(patch, layer), or compute_model_corner_uvs() for a ModelUV layer
std::vector<Vec2f> corner;
// False when `pos` is not on this layer's patch (farther than `tol`): there is no uv there, so the
// layer contributes nothing - the same as a non-tiled texture outside its placement.
@@ -3147,15 +3350,9 @@ struct LscmLookup {
if (d2 < 0.f || d2 > tol * tol || hit >= patch.indices.size())
return false;
const stl_triangle_vertex_indices &t = patch.indices[hit];
const Vec3f &a = patch.vertices[size_t(t[0])], &b = patch.vertices[size_t(t[1])], &c = patch.vertices[size_t(t[2])];
const Vec3f e0 = b - a, e1 = c - a, ep = foot - a;
const float d00 = e0.dot(e0), d01 = e0.dot(e1), d11 = e1.dot(e1), dp0 = ep.dot(e0), dp1 = ep.dot(e1);
const float den = d00 * d11 - d01 * d01;
float w1 = 1.f / 3.f, w2 = 1.f / 3.f; // a degenerate triangle takes its centroid's uv
if (std::abs(den) > 1e-20f) {
w1 = (d11 * dp0 - d01 * dp1) / den;
w2 = (d00 * dp1 - d01 * dp0) / den;
}
float w1 = 0.f, w2 = 0.f; // a degenerate triangle takes its centroid's uv
barycentric_weights(patch.vertices[size_t(t[0])], patch.vertices[size_t(t[1])], patch.vertices[size_t(t[2])],
foot, w1, w2);
const Vec2f *c3 = &corner[hit * 3];
uv = (1.f - w1 - w2) * c3[0] + w1 * c3[1] + w2 * c3[2];
return true;
@@ -3214,7 +3411,7 @@ struct PreparedLayer {
// where a layer is placed. `need_color` additionally drops layers that cannot contribute colour.
std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
const indexed_triangle_set &base_mesh, const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data, bool need_color)
const TextureDisplacementFacetsData &facets_data, bool need_color, const std::vector<Vec2f> *model_uv)
{
auto prepared = std::make_shared<std::vector<PreparedLayer>>();
@@ -3245,9 +3442,14 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
if (tex.empty() || (need_color && !tex.has_color()))
continue;
const bool model_uv_layer = layer->projection_method == TextureProjectionMethod::ModelUV &&
model_uv != nullptr && model_uv->size() == base_mesh.indices.size() * 3;
TriangleSelector selector(selector_mesh);
selector.deserialize(data, false);
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER);
std::vector<int> patch_source; // only the model's own channel needs it; see the classic path
const indexed_triangle_set patch =
selector.get_facets_strict(EnforcerBlockerType::ENFORCER, model_uv_layer ? &patch_source : nullptr);
if (patch.indices.empty())
continue;
@@ -3257,12 +3459,16 @@ std::shared_ptr<std::vector<PreparedLayer>> prepare_sampleable_layers(
texture_displacement_patch_frame(patch, vertex_normals, centroid, axis, average_normal);
std::shared_ptr<const LscmLookup> lscm;
if (layer->projection_method == TextureProjectionMethod::LSCM) {
if (model_uv_layer || layer->projection_method == TextureProjectionMethod::LSCM) {
// Solved on the very patch the classic path and the GUI solve it on (same geometry, seam
// angle and edges), so it hits the unwrap cache and lands exactly where the UV editor
// shows it, hand-placed islands and UV edits included.
// shows it, hand-placed islands and UV edits included. A ModelUV layer needs no solve at
// all - its corners are read straight out of the volume's channel - but it is looked up
// through the same per-triangle machinery, for the same reason: there is no formula from a
// point to its uv.
auto l = std::make_shared<LscmLookup>();
l->corner = compute_lscm_corner_uvs(patch, *layer);
l->corner = model_uv_layer ? compute_model_corner_uvs(base_mesh, *model_uv, patch, patch_source) :
compute_lscm_corner_uvs(patch, *layer);
if (l->corner.size() == patch.indices.size() * 3) {
l->patch = patch;
l->tree = AABBTreeIndirect::build_aabb_tree_over_indexed_triangle_set(l->patch.vertices, l->patch.indices);
@@ -3288,11 +3494,12 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize,
ColorQuantizeFn quantize_pure)
ColorQuantizeFn quantize_pure,
const std::vector<Vec2f> *model_uv)
{
if (!quantize)
return nullptr;
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true);
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ true, model_uv);
if (prepared->empty())
return nullptr;
// Per layer: a flat-colour image is matched against the filaments alone, when that quantizer
@@ -3323,9 +3530,10 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data)
const TextureDisplacementFacetsData &facets_data,
const std::vector<Vec2f> *model_uv)
{
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false);
auto prepared = prepare_sampleable_layers(base_mesh, layers, facets_data, /* need_color */ false, model_uv);
if (prepared->empty())
return nullptr;
@@ -5166,5 +5374,260 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
return out;
}
// ---------------------------------------------------------------------------------------------
// Project persistence. See the declarations in TextureDisplacement.hpp for why this is JSON rather
// than the cereal stream next to them.
// ---------------------------------------------------------------------------------------------
namespace {
nlohmann::json vec2_to_json(const Vec2f &v) { return nlohmann::json::array({ v.x(), v.y() }); }
nlohmann::json vec3_to_json(const Vec3f &v) { return nlohmann::json::array({ v.x(), v.y(), v.z() }); }
Vec2f vec2_from_json(const nlohmann::json &j, const Vec2f &fallback)
{
if (!j.is_array() || j.size() != 2)
return fallback;
return Vec2f(j[0].get<float>(), j[1].get<float>());
}
Vec3f vec3_from_json(const nlohmann::json &j, const Vec3f &fallback)
{
if (!j.is_array() || j.size() != 3)
return fallback;
return Vec3f(j[0].get<float>(), j[1].get<float>(), j[2].get<float>());
}
// Reads key `k` into `out` when it is present and of the expected type, leaving `out` alone otherwise.
// That "leave it alone" is the whole point: every member starts at its struct default, so a project
// written by an older build simply keeps the defaults for whatever it did not know about.
template<class T> void read(const nlohmann::json &j, const char *k, T &out)
{
const auto it = j.find(k);
if (it == j.end())
return;
try {
out = it->get<T>();
} catch (...) {
}
}
void read_enum(const nlohmann::json &j, const char *k, int &out)
{
const auto it = j.find(k);
if (it != j.end() && it->is_number_integer())
out = it->get<int>();
}
} // namespace
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementOptions &options)
{
nlohmann::json root;
root["version"] = 1;
nlohmann::json &opt = root["options"];
opt["displace_border"] = options.displace_border;
opt["smooth_enabled"] = options.smooth_enabled;
opt["smooth_strength"] = options.smooth_strength;
opt["smooth_iterations"] = options.smooth_iterations;
opt["smooth_skip_border"] = options.smooth_skip_border;
opt["pipeline_v2"] = options.pipeline_v2;
opt["v2_refine_mm"] = options.v2_refine_mm;
opt["v2_regularize"] = options.v2_regularize;
opt["v2_max_triangles_k"] = options.v2_max_triangles_k;
opt["v2_relocate"] = options.v2_relocate;
opt["v2_flip_edges"] = options.v2_flip_edges;
opt["color_mix_enabled"] = options.color_mix_enabled;
opt["color_despeckle"] = options.color_despeckle;
opt["color_mix_count"] = options.color_mix_count;
opt["active_slot"] = options.active_slot;
opt["panel_mode"] = options.panel_mode;
nlohmann::json &arr = root["layers"];
arr = nlohmann::json::array();
for (const TextureDisplacementLayer &l : layers) {
nlohmann::json j;
j["slot"] = l.slot;
j["name"] = l.name;
j["path"] = l.path;
j["path_in_3mf"] = l.path_in_3mf;
j["depth_mm"] = l.depth_mm;
j["tiling_scale"] = l.tiling_scale;
j["rotation_deg"] = l.rotation_deg;
j["offset"] = vec2_to_json(l.offset);
j["invert"] = l.invert;
j["midlevel"] = l.midlevel;
j["smoothing"] = l.smoothing;
j["edge_smoothing"] = l.edge_smoothing;
j["edge_smoothing_amount"] = l.edge_smoothing_amount;
j["auto_connect_islands"] = l.auto_connect_islands;
j["tile_enabled"] = l.tile_enabled;
j["tile_method"] = int(l.tile_method);
j["projection_method"] = int(l.projection_method);
j["blend_mode"] = int(l.blend_mode);
j["color_enabled"] = l.color_enabled;
j["lscm_seam_angle_deg"] = l.lscm_seam_angle_deg;
j["island_padding_mm"] = l.island_padding_mm;
j["view_project_right"] = vec3_to_json(l.view_project_right);
j["view_project_up"] = vec3_to_json(l.view_project_up);
j["view_project_projective"] = l.view_project_projective;
j["view_project_matrix"] = l.view_project_matrix;
// Flat pairs rather than nested arrays: shorter, and the reader can simply ignore a trailing
// odd element instead of having to validate every sub-array.
nlohmann::json &seams = j["lscm_seam_edges"];
seams = nlohmann::json::array();
for (const auto &[a, b] : l.lscm_seam_edges) {
seams.push_back(a);
seams.push_back(b);
}
nlohmann::json &islands = j["islands"];
islands = nlohmann::json::array();
for (const TextureIsland &island : l.islands)
islands.push_back(nlohmann::json::array(
{ island.offset.x(), island.offset.y(), island.rotation_deg, island.scale }));
j["island_groups"] = l.island_groups;
nlohmann::json &overrides = j["lscm_uv_overrides"];
overrides = nlohmann::json::array();
for (const auto &[key, uv] : l.lscm_uv_overrides)
overrides.push_back(nlohmann::json::array({ key, uv.x(), uv.y() }));
arr.push_back(std::move(j));
}
return root.dump();
}
bool texture_displacement_layers_from_json(const std::string &text,
std::vector<TextureDisplacementLayer> &layers,
TextureDisplacementOptions &options)
{
nlohmann::json root;
try {
root = nlohmann::json::parse(text);
} catch (...) {
return false;
}
if (!root.is_object())
return false;
TextureDisplacementOptions out_options;
if (const auto it = root.find("options"); it != root.end() && it->is_object()) {
const nlohmann::json &opt = *it;
read(opt, "displace_border", out_options.displace_border);
read(opt, "smooth_enabled", out_options.smooth_enabled);
read(opt, "smooth_strength", out_options.smooth_strength);
read(opt, "smooth_iterations", out_options.smooth_iterations);
read(opt, "smooth_skip_border", out_options.smooth_skip_border);
read(opt, "pipeline_v2", out_options.pipeline_v2);
read(opt, "v2_refine_mm", out_options.v2_refine_mm);
read(opt, "v2_regularize", out_options.v2_regularize);
read(opt, "v2_max_triangles_k", out_options.v2_max_triangles_k);
read(opt, "v2_relocate", out_options.v2_relocate);
read(opt, "v2_flip_edges", out_options.v2_flip_edges);
read(opt, "color_mix_enabled", out_options.color_mix_enabled);
read(opt, "color_despeckle", out_options.color_despeckle);
read(opt, "color_mix_count", out_options.color_mix_count);
read(opt, "active_slot", out_options.active_slot);
read(opt, "panel_mode", out_options.panel_mode);
out_options.active_slot = std::clamp(out_options.active_slot, 0, int(TEXTURE_DISPLACEMENT_MAX_LAYERS) - 1);
out_options.panel_mode = std::clamp(out_options.panel_mode, 0, 1);
}
std::vector<TextureDisplacementLayer> out_layers;
// read() skips a value of the wrong type, but the vector, matrix, seam, island and UV-override arrays
// below read their numbers with get<>(), which throws on one; that rejects the document, like text that
// does not parse.
try {
if (const auto it = root.find("layers"); it != root.end() && it->is_array()) {
for (const nlohmann::json &j : *it) {
if (!j.is_object())
continue;
TextureDisplacementLayer l;
read(j, "slot", l.slot);
// The panel and the paint masks are indexed by slot, so a layer without a valid one rejects the
// document rather than reaching them.
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
return false;
read(j, "name", l.name);
read(j, "path", l.path);
read(j, "path_in_3mf", l.path_in_3mf);
read(j, "depth_mm", l.depth_mm);
read(j, "tiling_scale", l.tiling_scale);
read(j, "rotation_deg", l.rotation_deg);
if (const auto o = j.find("offset"); o != j.end())
l.offset = vec2_from_json(*o, l.offset);
read(j, "invert", l.invert);
read(j, "midlevel", l.midlevel);
read(j, "smoothing", l.smoothing);
read(j, "edge_smoothing", l.edge_smoothing);
read(j, "edge_smoothing_amount", l.edge_smoothing_amount);
read(j, "auto_connect_islands", l.auto_connect_islands);
read(j, "tile_enabled", l.tile_enabled);
read(j, "color_enabled", l.color_enabled);
int tile_method = int(l.tile_method), projection = int(l.projection_method), blend = int(l.blend_mode);
read_enum(j, "tile_method", tile_method);
read_enum(j, "projection_method", projection);
read_enum(j, "blend_mode", blend);
l.tile_method = TextureTileMethod(tile_method);
l.projection_method = TextureProjectionMethod(std::clamp(projection, 0, 4));
l.blend_mode = TextureBlendMode(blend);
read(j, "lscm_seam_angle_deg", l.lscm_seam_angle_deg);
read(j, "island_padding_mm", l.island_padding_mm);
if (const auto v = j.find("view_project_right"); v != j.end())
l.view_project_right = vec3_from_json(*v, l.view_project_right);
if (const auto v = j.find("view_project_up"); v != j.end())
l.view_project_up = vec3_from_json(*v, l.view_project_up);
read(j, "view_project_projective", l.view_project_projective);
if (const auto v = j.find("view_project_matrix"); v != j.end() && v->is_array() && v->size() == 12)
for (size_t i = 0; i < 12; ++i)
l.view_project_matrix[i] = (*v)[i].get<float>();
if (const auto v = j.find("lscm_seam_edges"); v != j.end() && v->is_array())
for (size_t i = 0; i + 1 < v->size(); i += 2)
l.lscm_seam_edges.emplace_back((*v)[i].get<int>(), (*v)[i + 1].get<int>());
if (const auto v = j.find("islands"); v != j.end() && v->is_array())
for (const nlohmann::json &e : *v) {
if (!e.is_array() || e.size() != 4)
continue;
TextureIsland island;
island.offset = Vec2f(e[0].get<float>(), e[1].get<float>());
island.rotation_deg = e[2].get<float>();
island.scale = e[3].get<float>();
l.islands.push_back(island);
}
read(j, "island_groups", l.island_groups);
if (const auto v = j.find("lscm_uv_overrides"); v != j.end() && v->is_array())
for (const nlohmann::json &e : *v) {
if (!e.is_array() || e.size() != 3)
continue;
l.lscm_uv_overrides.emplace_back(e[0].get<int>(), Vec2f(e[1].get<float>(), e[2].get<float>()));
}
out_layers.push_back(std::move(l));
}
}
} catch (const nlohmann::json::exception &) {
return false;
}
layers = std::move(out_layers);
options = out_options;
return true;
}
} // namespace Slic3r
+142 -5
View File
@@ -86,6 +86,17 @@ enum class TextureProjectionMethod : int
// 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,
// The model's own texture coordinates, as the file it was imported from carried them (see
// ModelVolume::texture_uv). Nothing is unwrapped or projected: the artist's layout *is* the
// placement, so a texture authored against that layout - the model's own albedo map, say - lands
// exactly where it was painted. Automatically picked for a new layer on a volume that has a UV
// channel, since re-unwrapping a model that already carries one can only be worse than using it.
//
// The uv is already normalized, so unlike every method above it is not measured in millimetres:
// "Tile size" and the non-square aspect correction do not apply to it (the layer's rotation and
// offset still do). Falls back to Triplanar when the volume has no channel, exactly as LSCM falls
// back when a patch cannot be flattened.
ModelUV = 5,
};
// Dihedral angle (degrees) above which an edge between two painted triangles becomes a chart seam
@@ -121,6 +132,11 @@ enum class TextureBlendMode : int
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)
// acc = value: this layer replaces what is under it instead of combining with it, so where it is
// painted only its own relief remains. The image editor's "Normal" mode, under the name the panel
// uses. Note the stack's *colour* already behaves this way - a higher slot's colour overwrites a
// lower one's where they overlap - so this is what makes the displacement agree with it.
Override = 4,
};
// Combines one layer's signed displacement `value` (mm) into `accumulated` (mm) per `mode`.
@@ -397,14 +413,41 @@ struct TextureDisplacementOptions
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
int color_despeckle = 2;
// Where the panel was left, rather than anything the bake reads. It lives with the volume because
// that is what it describes: reopening a project should put the user back on the layer they were
// editing, in the mode they were editing it in, not on slot 0 in Standard.
int active_slot = 0;
int panel_mode = 0; // 0 Standard, 1 Pro
template<class Archive> void serialize(Archive &ar)
{
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count,
active_slot, panel_mode);
}
};
// Project persistence (see bbs_3mf.cpp). The layer stack and the per-volume options are written to the
// .3mf as JSON rather than through the cereal save()/load() above: those two are positional and
// unversioned, which is right for the undo/redo stack they serve (one session, one binary) but would
// make every future field a project-breaking change. A JSON object tolerates both directions - an
// unknown key is ignored, a missing one keeps the member's default - so old projects keep loading and
// new ones degrade gracefully in older builds.
//
// The texture image itself is *not* in here. It is a binary blob that belongs in the archive as a file
// of its own, exactly as EmbossShape stores its SVG; `path_in_3mf` names that file, and the caller is
// responsible for writing it and for filling `image_data` back in on load.
std::string texture_displacement_layers_to_json(const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementOptions &options);
// Returns false and leaves both outputs untouched when the text is not a valid JSON object, a number in a
// layer's vector, matrix, seam, island or UV-override array has the wrong type, or a layer's slot is
// missing or outside [0, TEXTURE_DISPLACEMENT_MAX_LAYERS). Any other value of the wrong type or shape
// keeps its default.
bool texture_displacement_layers_from_json(const std::string &text,
std::vector<TextureDisplacementLayer> &layers,
TextureDisplacementOptions &options);
// How much detail a height texture carries: central differences of the grey image, the mean gradient
// and the share of texels steeper than 30 grey levels, mapped to how many texels one mesh edge may
// span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
@@ -541,7 +584,13 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// 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);
//
// `normalized_uv` says the coordinate is already a uv rather than a millimetre measurement on the
// model - which is what TextureProjectionMethod::ModelUV hands in. Then neither the tiling scale (mm
// per tile is meaningless for it) nor the aspect correction (the artist's layout already accounts for
// the image's proportions) applies, and only the rotation and offset are left.
Vec2f apply_uv_transform(const Vec2f &planar, const TextureDisplacementLayer &layer, float aspect = 1.f,
bool normalized_uv = false);
// 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 -
@@ -578,6 +627,31 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr);
// Carries a per-facet paint annotation - the colour paint above all - from a mesh onto the mesh a
// bake produced from it: every triangle of `dst_mesh` takes the state painted at its own foot point on
// `src_mesh`, i.e. on the nearest point of the surface it was built from. Returned as one
// EnforcerBlockerType per `dst_mesh` triangle, 0 (NONE) where that foot point carried no paint.
//
// ModelVolume::restore_painting() cannot do this. Its remap pairs a source paint triangle with a
// target triangle that *overlaps* it to within 0.01 mm, which is right for a remesh or a
// simplification - they leave the surface where it was - and useless after a bake, which moves the
// surface by the relief depth. Measured on a model displaced by a tenth of its own size, that remap
// kept a quarter of the colour and left the rest unpainted; this keeps essentially all of it, because
// a foot point is exactly as far away as the relief is deep and the lookup does not care how far that
// is.
//
// The source's *sub*-triangles are what is looked up, not its triangles, so the part of a triangle a
// brush stroke only clipped a corner off keeps its own state rather than rounding to the whole
// triangle's. No distance limit is needed or wanted: the nearest point on the source surface is where
// a baked triangle came from however far the relief carried it, and an unpainted origin reports NONE
// on its own.
//
// Returns an empty vector when `src_paint` is empty or either mesh is - the caller then has nothing to
// carry, which is not the same as "carry nothing".
std::vector<uint8_t> transfer_facet_paint(const indexed_triangle_set &src_mesh,
const TriangleSelector::TriangleSplittingData &src_paint,
const indexed_triangle_set &dst_mesh);
// 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
@@ -691,6 +765,55 @@ std::vector<Vec2f> compute_lscm_uvs(const indexed_triangle_set &patch, const Tex
// Returns an empty vector if the patch has no triangles or the unwrap carries no source_face map.
std::vector<Vec2f> compute_lscm_corner_uvs(const indexed_triangle_set &patch, const TextureDisplacementLayer &layer);
// Three UVs per patch triangle, read out of the model's *own* UV channel - the
// TextureProjectionMethod::ModelUV counterpart of compute_lscm_corner_uvs(), and laid out the same way
// (corner k of triangle i at index 3i+k).
//
// `model_uv` is the volume's channel: three uvs per triangle of `base_mesh`, in its triangle order
// (see ModelVolume::texture_uv). `patch_source` maps each patch triangle back to the base-mesh
// triangle it was cut from - TriangleSelector::get_facets_strict()'s out_source. A patch triangle is
// always a piece of exactly one base triangle, so each of its corners is interpolated barycentrically
// inside that triangle's three uvs. That is what keeps a sub-triangle the brush split (whose corners
// are not base-mesh vertices at all) on the texture, and what keeps the two sides of a UV seam
// reading from their own island instead of from one averaged coordinate.
//
// Returns an empty vector when the channel does not match `base_mesh`, when `patch_source` does not
// match `patch`, or when either is empty - the callers treat that as "no placement" and fall back,
// exactly as they do for an unwrap that failed.
std::vector<Vec2f> compute_model_corner_uvs(const indexed_triangle_set &base_mesh,
const std::vector<Vec2f> &model_uv,
const indexed_triangle_set &patch,
const std::vector<int> &patch_source);
// One UV per patch vertex, collapsed from a per-corner array: the first corner to reach a vertex wins.
// This is the per-corner/per-vertex split compute_lscm_uvs() documents, applied to any corner array -
// displacement is per-vertex, so a vertex on a seam has to settle on one of its copies.
// Returns an empty vector if `corner` is not three per patch triangle.
std::vector<Vec2f> collapse_corner_uvs(const indexed_triangle_set &patch, const std::vector<Vec2f> &corner);
// Resamples a per-corner UV channel from `src_mesh` onto `dst_mesh`, for when something replaces a
// volume's mesh and the model's own UV channel has to come along: the colour import's painted mesh,
// the gizmo's remesh/subdivide, the default bake pipeline's rebuild. Each destination corner takes the
// uv of its closest point on the source surface, interpolated barycentrically inside the source
// triangle it lands on - the same transfer LscmLookup does per point, run over every corner.
//
// Exact wherever the two meshes describe the same surface, which is the case for every caller here
// (they subdivide, repair or displace it, they do not reshape it). A corner that lands nowhere near
// the source - farther than `max_distance` (negative: no limit) - takes (0, 0).
//
// One thing a positional transfer cannot reproduce: a *seam* vertex, where one position carries a
// different uv in each island that meets there. Position alone does not say which island a corner
// belongs to, so corners on a seam all take whichever island the nearest-triangle lookup lands on,
// and the texture can be off by up to one triangle in a band along the island borders. Doing better
// needs a triangle-to-triangle correspondence, which only some of the stages that replace a mesh
// have (a remesh has none at all).
//
// Returns an empty vector when `src_uv` is not three per source triangle, or when either mesh is empty.
std::vector<Vec2f> transfer_uv_channel(const indexed_triangle_set &src_mesh,
const std::vector<Vec2f> &src_uv,
const indexed_triangle_set &dst_mesh,
float max_distance = -1.f);
// The TextureDisplacementLayer::lscm_uv_overrides key for one unwrapped vertex (an index into PatchUnwrap::uvs).
inline int lscm_uv_override_key(int unwrapped_vertex) { return -(unwrapped_vertex + 1); }
@@ -749,6 +872,10 @@ using TextureDisplacementFacetsData = std::array<TriangleSelector::TriangleSplit
//
// `color`, when given, also reports which filament each triangle should print in - see
// TextureColorRequest.
//
// `model_uv`, when given, is the volume's own UV channel (three uvs per base-mesh triangle, see
// ModelVolume::texture_uv) and is what a TextureProjectionMethod::ModelUV layer maps through. A layer
// set to ModelUV without it falls back to Triplanar.
struct TextureColorRequest
{
// RGB -> palette index. Supplied by the GUI, which owns both the perceptual matching and the list
@@ -823,7 +950,9 @@ indexed_triangle_set build_texture_displacement(const indexed_triangle_set
// stage, already brought back into `base_mesh`'s frame.
BakeStageRecorder *debug = nullptr,
// When given, receives what the bake spent.
TextureBakeStats *stats = nullptr);
TextureBakeStats *stats = nullptr,
// The model's own UV channel; see above.
const std::vector<Vec2f> *model_uv = nullptr);
// `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
@@ -898,11 +1027,15 @@ ColorFieldSampler make_combined_color_sampler(const indexed_triangle_set
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data,
ColorQuantizeFn quantize,
ColorQuantizeFn quantize_pure = nullptr);
ColorQuantizeFn quantize_pure = nullptr,
// The model's own UV channel, for ModelUV layers; see
// build_texture_displacement().
const std::vector<Vec2f> *model_uv = nullptr);
HeightFieldSampler make_combined_displacement_sampler(const indexed_triangle_set &base_mesh,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementFacetsData &facets_data);
const TextureDisplacementFacetsData &facets_data,
const std::vector<Vec2f> *model_uv = nullptr);
// 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
@@ -1107,6 +1240,10 @@ struct TextureDisplacementPrepareResult
{
indexed_triangle_set mesh;
TextureDisplacementFacetsData masks;
// The model's own UV channel, resampled onto `mesh` (see transfer_uv_channel()). Empty when the
// volume had none. Like the masks it is handed back rather than left to the caller to recompute,
// because only the run itself knows which stages replaced the mesh.
std::vector<Vec2f> uv;
// 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;
+21
View File
@@ -25,6 +25,7 @@
#include "TextureToColor/ColorUtils.hpp"
#include "Model.hpp"
#include "TextureDisplacement.hpp"
#include "TextureToColor/TriMesh.hpp"
#include "TriangleMesh.hpp"
#include "TriangleSelector.hpp"
@@ -554,9 +555,29 @@ bool apply_painted_mesh_to_volume(
<< ", unit_scale=" << unit_scale << ")";
}
// The model's own UV channel is indexed by triangle, and this replaces the triangles, so it has
// to be resampled onto them. Kept rather than dropped because it is the layout the model was
// authored against: a texture-displacement layer set to ModelUV maps through it, and the colour
// import is exactly the path a model that carries one arrives by.
const indexed_triangle_set old_its = volume.has_texture_uv() ? volume.mesh().its : indexed_triangle_set{};
const std::vector<Vec2f> old_uv = volume.texture_uv;
volume.set_mesh(std::move(new_mesh));
volume.calculate_convex_hull();
// Resampled here and not further down, while the two meshes still share a frame: the painted mesh
// was just shifted by the same source.mesh_offset the old one was centred by, so the only thing
// between them is whatever drift the repair introduced. The re-centering below translates the mesh
// again, and a translation is exactly what a closest-point transfer cannot see through - it would
// read every uv off a point that far away. A uv is a coordinate in the texture, so it does not care
// about either shift; it just has to be read before the second one happens.
if (!old_its.indices.empty())
volume.texture_uv = transfer_uv_channel(old_its, old_uv, volume.mesh().its);
else
// No channel to carry, so make sure none is left behind either: a leftover of the right length
// would read as a valid channel against a mesh it was never measured on.
volume.reset_texture_uv();
// Re-center the replaced mesh so its bbox center sits at the origin,
// matching what center_geometry_after_creation did for the original mesh.
// CGAL repair / subdivision may shift the bbox center (drift); without
+1 -4
View File
@@ -1,5 +1,4 @@
#ifndef libslic3r_Timer_hpp_
#define libslic3r_Timer_hpp_
#pragma once
#include "TriangleMesh.hpp"
@@ -10,5 +9,3 @@ public:
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
};
} // namespace Slic3r
#endif // libslic3r_Timer_hpp_
-1
View File
@@ -109,7 +109,6 @@ extern std::string format_memsize_MB(size_t n);
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1);
// Return string to be added to the boost::log output to inform about the current process memory allocation.
// The string is non-empty if the loglevel >= info (3) or ignore_loglevel==true.
// Latter is used to get the memory info from SysInfoDialog.
extern std::string log_memory_info(bool ignore_loglevel = false);
extern void disable_multi_threading();
// Returns the size of physical memory (RAM) in bytes.
+1 -3
View File
@@ -1744,9 +1744,7 @@ std::string format_diameter_to_str(double diameter, int precision)
return oss.str();
}
// Returns platform-specific string to be used as log output or parsed in SysInfoDialog.
// The latter parses the string with (semi)colons as separators, it should look about as
// "desc1: value1; desc2: value2" or similar (spaces should not matter).
// Returns platform-specific string to be used as log output.
std::string log_memory_info(bool ignore_loglevel)
{
std::string out;
-3
View File
@@ -416,8 +416,6 @@ set(SLIC3R_GUI_SOURCES
GUI/MultiSendMachineModel.hpp
GUI/MultiTaskManagerPage.cpp
GUI/MultiTaskManagerPage.hpp
GUI/MultiTaskModel.cpp
GUI/MultiTaskModel.hpp
GUI/NetworkTestDialog.cpp
GUI/NetworkTestDialog.hpp
GUI/Notebook.cpp
@@ -802,7 +800,6 @@ set(SLIC3R_GUI_SOURCES
Utils/PrintHost.hpp
Utils/Process.cpp
Utils/Process.hpp
Utils/Profile.hpp
Utils/RaycastManager.cpp
Utils/RaycastManager.hpp
Utils/Repetier.cpp
+173 -49
View File
@@ -16,6 +16,7 @@
#include <wx/checklst.h>
#include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/DropDown.hpp"
#include "slic3r/GUI/Widgets/Label.hpp"
#include <utility>
#include "slic3r/GUI/wxExtensions.hpp"
@@ -172,7 +173,26 @@ void AMSMaterialsSetting::create()
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, this);
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Bind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
// Favorites toggled in the open list are regrouped just before it opens again, so rows never move
// under the cursor. These handlers run ahead of ComboBox's own, which then measure and show the
// rebuilt rows; wxEVT_COMBOBOX_DROPDOWN would be too late, as ComboBox sends it after showing the list.
auto regroup_favorites = [this]() {
if (!m_favorites_regroup || m_comboBox_filament->is_drop_down())
return;
const FilamentInfos* info = selected_filament_info();
const std::string filament_id = info ? info->filament_id : std::string();
rebuild_filament_dropdown_items(filament_id);
};
m_comboBox_filament->Bind(wxEVT_LEFT_DOWN, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
m_comboBox_filament->Bind(wxEVT_LEFT_DCLICK, [regroup_favorites](wxMouseEvent& e) { regroup_favorites(); e.Skip(); });
m_comboBox_filament->Bind(wxEVT_KEY_DOWN, [regroup_favorites](wxKeyEvent& e) {
if (e.GetKeyCode() == WXK_RETURN || e.GetKeyCode() == WXK_SPACE) // the keys that open the list
regroup_favorites();
e.Skip();
});
m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -191,11 +211,9 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0);
#ifdef __APPLE__
m_comboBox_filament = new wxComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
#else
m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
#endif
m_star_filled = ScalableBitmap(this, "score_star_light", 16);
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
@@ -420,6 +438,7 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
AMSMaterialsSetting::~AMSMaterialsSetting()
{
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this);
m_comboBox_filament->Unbind(EVT_DROPDOWN_ITEM_ACTION, &AMSMaterialsSetting::on_toggle_favorite_filament, this);
m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
}
@@ -531,10 +550,9 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
std::string selected_ams_id;
PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
const FilamentInfos* selected_info = selected_filament_info();
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
std::string filament_id = filament_item.filament_id;
if (it->filament_id.compare(filament_id) == 0) {
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) {
selected_ams_id = it->filament_id;
break;
}
@@ -682,12 +700,13 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
ams_setting_id = "";
// the combobox item
auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
const FilamentInfos* selected_info = selected_filament_info();
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
// check filament info (rack-gated per-nozzle blacklist evaluation)
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
filament_item.filament_id, ams_filament_id, ams_setting_id);
filament_id, ams_filament_id, ams_setting_id);
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
wxString info_msg;
@@ -986,6 +1005,133 @@ static void _collect_filament_info(const wxString& shown_name,
query_filament_types[shown_name] = filament.config.get_filament_type();
}
static const char* AMS_FAVORITE_FILAMENT_IDS_KEY = "ams_favorite_filament_ids";
const AMSMaterialsSetting::FilamentInfos* AMSMaterialsSetting::selected_filament_info() const
{
const int selection = m_comboBox_filament->GetSelection();
if (selection < 0 || selection >= int(m_filament_list_items.size()) || !m_filament_list_items[selection].is_filament)
return nullptr;
return &m_filament_list_items[selection].filament;
}
std::set<std::string> AMSMaterialsSetting::load_favorite_filament_ids() const
{
std::set<std::string> favorite_ids;
std::vector<std::string> values;
const std::string serialized = wxGetApp().app_config->get(AMS_FAVORITE_FILAMENT_IDS_KEY);
if (!serialized.empty() && Slic3r::unescape_strings_cstyle(serialized, values)) {
favorite_ids.insert(values.begin(), values.end());
}
return favorite_ids;
}
void AMSMaterialsSetting::save_favorite_filament_ids(const std::set<std::string>& favorite_ids)
{
std::vector<std::string> values(favorite_ids.begin(), favorite_ids.end());
wxGetApp().app_config->set(AMS_FAVORITE_FILAMENT_IDS_KEY, Slic3r::escape_strings_cstyle(values));
wxGetApp().app_config->save();
}
void AMSMaterialsSetting::rebuild_filament_dropdown_items(const std::string& selected_filament_id)
{
std::vector<DropDown::Item> items;
m_filament_list_items.clear();
auto append_header = [&items, this](const wxString& label) {
DropDown::Item item;
item.text = label;
item.style = DD_ITEM_STYLE_SPLIT_ITEM | DD_ITEM_STYLE_DISABLED;
items.push_back(item);
m_filament_list_items.push_back(FilamentListItem{});
};
auto append_filament = [&items, this](const wxString& name) {
auto info_it = map_filament_items.find(name);
if (info_it == map_filament_items.end())
return;
DropDown::Item item;
item.text = name;
items.push_back(item);
m_filament_list_items.push_back(FilamentListItem{info_it->second, true});
};
// Favorites are listed again at the top under their own header. Without any, the list is the
// plain filament list.
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
std::vector<wxString> favorites;
for (const wxString& name : m_sorted_filament_items) {
auto info_it = map_filament_items.find(name);
if (info_it != map_filament_items.end() && favorite_ids.count(info_it->second.filament_id) > 0)
favorites.push_back(name);
}
if (!favorites.empty()) {
append_header(_L("Favorite filaments"));
for (const wxString& name : favorites)
append_filament(name);
append_header(_L("All filaments"));
}
for (const wxString& name : m_sorted_filament_items)
append_filament(name);
int selection_idx = -1;
if (!selected_filament_id.empty()) {
for (int i = 0; i < int(m_filament_list_items.size()); ++i) {
if (m_filament_list_items[i].is_filament && m_filament_list_items[i].filament.filament_id == selected_filament_id) {
selection_idx = i;
break;
}
}
}
// Clear() first: SetItems() keeps the old selection, and SetSelection() ignores an index equal to it,
// which would leave the previous filament's name on the combo.
m_comboBox_filament->Clear();
m_comboBox_filament->SetItems(items);
update_favorite_stars();
m_comboBox_filament->SetSelection(selection_idx);
m_favorites_regroup = false;
}
void AMSMaterialsSetting::update_favorite_stars()
{
const std::set<std::string> favorite_ids = load_favorite_filament_ids();
for (size_t i = 0; i < m_filament_list_items.size(); ++i) {
const FilamentListItem& row = m_filament_list_items[i];
if (row.is_filament) {
const bool favorite = favorite_ids.count(row.filament.filament_id) > 0;
m_comboBox_filament->SetItemActionBitmap(i, favorite ? m_star_filled.bmp() : m_star_empty.bmp());
}
}
}
void AMSMaterialsSetting::toggle_favorite_filament(const std::string& filament_id)
{
if (filament_id.empty())
return;
std::set<std::string> favorite_ids = load_favorite_filament_ids();
auto iter = favorite_ids.find(filament_id);
if (iter == favorite_ids.end())
favorite_ids.insert(filament_id);
else
favorite_ids.erase(iter);
save_favorite_filament_ids(favorite_ids);
}
void AMSMaterialsSetting::on_toggle_favorite_filament(wxCommandEvent& evt)
{
const int index = evt.GetInt();
if (index < 0 || index >= int(m_filament_list_items.size()) || !m_filament_list_items[index].is_filament)
return;
toggle_favorite_filament(m_filament_list_items[index].filament.filament_id);
update_favorite_stars();
m_favorites_regroup = true;
}
void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
{
if (!obj) return;
@@ -999,13 +1145,12 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
m_input_k_val->GetTextCtrl()->SetValue(k);
m_input_n_val->GetTextCtrl()->SetValue(n);
wxArrayString filament_items;
wxString bambu_filament_name;
wxString hint_filament_name; // the hint type to be selected
std::unordered_map<wxString, wxString> query_filament_vendors;// some information for sort
std::unordered_map<wxString, wxString> query_filament_types; //
std::set<std::string> filament_id_set;
m_sorted_filament_items.clear();
// The alias keyed map has to start empty: it is a member, so a stale alias left by an earlier
// popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
map_filament_items.clear();
@@ -1035,7 +1180,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
if (alias.empty())
continue;
filament_items.push_back(alias);
m_sorted_filament_items.push_back(alias);
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
FilamentInfos filament_infos;
@@ -1044,7 +1189,6 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
map_filament_items[alias] = filament_infos;
if (filament_it->filament_id == ams_filament_id) {
hint_filament_name = from_u8(alias);
bambu_filament_name = from_u8(alias);
@@ -1215,24 +1359,11 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
return name_compare != 0 ? name_compare < 0 : left < right;
};
std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter);
}
// traverse the hint selection idx
int selection_idx = -1;
{
for(int i = 0; i < filament_items.size(); i++)
{
if (hint_filament_name == filament_items[i])
{
selection_idx = i;
break;
}
}
}
m_comboBox_filament->Set(filament_items);
m_comboBox_filament->SetSelection(selection_idx);
rebuild_filament_dropdown_items(ams_filament_id);
int selection_idx = m_comboBox_filament->GetSelection();
post_select_event(selection_idx);
if (selection_idx < 0) {
@@ -1289,6 +1420,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
m_filament_type = "";
const FilamentInfos* selected_info = selected_filament_info();
PresetBundle* preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) {
std::ostringstream stream;
@@ -1309,9 +1441,8 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
std::string nozzle_diameter_str = stream.str();
// Resolve the selection against the same list Popup() built the dropdown from, so the two
// halves of the dialog cannot disagree about which filaments this machine can use.
const std::string selected = m_comboBox_filament->GetValue().ToStdString();
if (!selected.empty()) {
const std::string filament_id = map_filament_items[selected].filament_id;
if (selected_info) {
const std::string& filament_id = selected_info->filament_id;
for (Preset *it : preset_bundle->get_filament_presets_for_machine(
DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
if (it->filament_id != filament_id)
@@ -1364,7 +1495,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
//reset cali
int cali_select_idx = -1;
if ( !this->obj || m_filament_selection < 0) {
if ( !this->obj || m_filament_selection < 0 || !selected_info) {
m_input_k_val->Enable(false);
m_input_n_val->Enable(false);
m_button_confirm->Disable(); // ORCA No need to change style
@@ -1383,21 +1514,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
ams_filament_id = "";
ams_setting_id = "";
if (preset_bundle) {
for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
auto itor = map_filament_items.find(m_comboBox_filament->GetValue().ToStdString());
if ( itor != map_filament_items.end()) {
ams_filament_id = itor->second.filament_id;
ams_setting_id = itor->second.setting_id;
break;
}
if (it->alias.compare(m_comboBox_filament->GetValue().ToStdString()) == 0) {
ams_filament_id = it->filament_id;
ams_setting_id = it->setting_id;
break;
}
}
if (selected_info) {
ams_filament_id = selected_info->filament_id;
ams_setting_id = selected_info->setting_id;
}
wxArrayString items;
@@ -1529,6 +1648,11 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
m_button_reset->Rescale(); // ORCA
m_button_confirm->Rescale(); // ORCA
m_button_close->Rescale(); // ORCA
// An open list would keep the measurements of the old stars, so close it before replacing them.
m_comboBox_filament->CloseDropdown();
m_star_filled.msw_rescale();
m_star_empty.msw_rescale();
update_favorite_stars();
this->Refresh();
}
+21 -6
View File
@@ -31,6 +31,7 @@
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <wx/panel.h>
#include <map>
#include <set>
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
@@ -156,6 +157,7 @@ protected:
void create_panel_kn(wxWindow* parent);
void on_dpi_changed(const wxRect &suggested_rect) override;
void on_select_filament(wxCommandEvent& evt);
void on_toggle_favorite_filament(wxCommandEvent& evt);
void on_select_cali_result(wxCommandEvent &evt);
void on_select_ok(wxCommandEvent &event);
void on_select_reset(wxCommandEvent &event);
@@ -163,6 +165,11 @@ protected:
void on_clr_picker(wxMouseEvent &event);
bool is_virtual_tray();
void update_widgets();
void rebuild_filament_dropdown_items(const std::string& selected_filament_id);
void update_favorite_stars();
std::set<std::string> load_favorite_filament_ids() const;
void save_favorite_filament_ids(const std::set<std::string>& favorite_ids);
void toggle_favorite_filament(const std::string& filament_id);
void update_filament_editing(bool is_printing);
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
@@ -202,12 +209,7 @@ protected:
int m_pa_cali_select_id = 0;
// Orca: native wxComboBox on macOS (the custom ComboBox mis-renders the filament dropdown there)
#ifdef __APPLE__
wxComboBox *m_comboBox_filament;
#else
ComboBox *m_comboBox_filament;
#endif
ComboBox * m_comboBox_cali_result;
TextInput* m_readonly_filament;
@@ -215,7 +217,20 @@ protected:
std::string filament_id;
std::string setting_id;
};
std::map<std::string, FilamentInfos> map_filament_items;
struct FilamentListItem {
FilamentInfos filament;
bool is_filament{false};
};
const FilamentInfos* selected_filament_info() const;
// Keyed by the same wxString the dropdown shows, so a row's name never has to be converted back.
std::map<wxString, FilamentInfos> map_filament_items;
std::vector<wxString> m_sorted_filament_items;
std::vector<FilamentListItem> m_filament_list_items;
ScalableBitmap m_star_filled;
ScalableBitmap m_star_empty;
// Favorites changed while the list was open; their rows are regrouped before it opens again.
bool m_favorites_regroup{false};
};
wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
+1 -4
View File
@@ -1,5 +1,4 @@
#ifndef slic3r_GUI_SingleChoice_hpp_
#define slic3r_GUI_SingleChoice_hpp_
#pragma once
#include "GUI_Utils.hpp"
#include "Plater.hpp"
@@ -33,5 +32,3 @@ private:
};
}} // namespace Slic3r::GUI
#endif
-19
View File
@@ -1,19 +0,0 @@
#include <exception>
#include <stdexcept>
#include "libslic3r/Exception.hpp"
namespace Slic3r {
class ConfigError : public Slic3r::RuntimeError {
using Slic3r::RuntimeError::RuntimeError;
};
namespace GUI {
class ConfigGUITypeError : public ConfigError {
using ConfigError::ConfigError;
};
} // namespace GUI
} // namespace Slic3r
+7 -6
View File
@@ -3232,9 +3232,16 @@ bool GUI_App::on_init_inner()
#ifdef _MSW_DARK_MODE
#ifndef __WINDOWS__
// The label colours and the dark mode flag were set above from the saved mode. When the system theme
// differs from it, refresh them, or dialogs that map their colours stay in the old mode for this session.
const bool saved_dark_mode = dark_mode();
wxSystemAppearance app = wxSystemSettings::GetAppearance();
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
GUI::wxGetApp().app_config->save();
if (dark_mode() != saved_dark_mode) {
init_label_colours();
Update_dark_mode_flag();
}
#endif // __APPLE__
@@ -4855,12 +4862,6 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit";
}
void GUI_App::system_info()
{
//SysInfoDialog dlg;
//dlg.ShowModal();
}
void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
{
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
-1
View File
@@ -519,7 +519,6 @@ public:
void check_printer_presets();
void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const;
+11 -8
View File
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item);
if (obj_idx != -1) {
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
(*m_objects)[obj_idx]->ensure_on_bed();
cnv->reload_scene(true, true);
update_info_items(obj_idx);
@@ -2779,12 +2779,13 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
GLCanvas3D* cnv = plater->canvas3D();
switch (type) {
case InfoItemType::CustomSupports:
case InfoItemType::CustomSupports: {
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot(plater, "Remove support painting");
Plater::TakeSnapshot snapshot(plater, "Remove support painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->supported_facets.reset();
break;
}
// BBS: remove CustomSeam
@@ -2795,19 +2796,21 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
}
break;
case InfoItemType::MmSegmentation:
case InfoItemType::MmSegmentation: {
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot(plater, "Remove color painting");
Plater::TakeSnapshot snapshot(plater, "Remove color painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->mmu_segmentation_facets.reset();
break;
}
case InfoItemType::FuzzySkin:
case InfoItemType::FuzzySkin: {
cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin"));
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->fuzzy_skin_facets.reset();
break;
}
// BBS: remove Sinking
case InfoItemType::Undef : assert(false); break;
@@ -6718,7 +6721,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
else if (col == colSinking) {
Plater * plater = wxGetApp().plater();
GLCanvas3D *cnv = plater->canvas3D();
Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed"));
int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item);
(*m_objects)[obj_idx]->ensure_on_bed();
@@ -494,7 +494,17 @@ bool GLGizmoTextureDisplacement::on_init()
{ctrl + _L("Mouse wheel"), m_desc["gap_area"]},
toggle_wireframe_shortcut
};
// Both ways into the same input box on any slider in the panel: ImGui turns a slider into one when
// it is Ctrl+clicked or when it is given the keyboard focus, which is what the edit icon asks for.
m_desc["type_value"] = _L("Type an exact value into a slider");
m_shortcuts_sliders = {
{ctrl + _L("Left mouse button"), m_desc["type_value"]},
{_L("Edit icon at a slider's right end"), m_desc["type_value"]},
};
for (std::vector<std::pair<wxString, wxString>> *list : {&m_shortcuts_brush, &m_shortcuts_bucket_fill, &m_shortcuts_gap_fill})
list->insert(list->end(), m_shortcuts_sliders.begin(), m_shortcuts_sliders.end());
return true;
}
@@ -1221,9 +1231,33 @@ indexed_triangle_set GLGizmoTextureDisplacement::patch_in_world(const indexed_tr
return world;
}
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer) const
std::vector<Vec2f> GLGizmoTextureDisplacement::model_uv_corners(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const
{
const ModelVolume *mv = texture_volume();
if (layer.projection_method != TextureProjectionMethod::ModelUV || patch_source == nullptr || mv == nullptr ||
!mv->has_texture_uv())
return {};
// The channel is indexed against the volume's *local* mesh, and a uv is a coordinate in the
// texture rather than on the model, so unlike every projected method this never goes through
// patch_in_world() - there is no millimetre measurement here for a world scale to change.
std::vector<Vec2f> uv = compute_model_corner_uvs(mv->mesh().its, mv->texture_uv, local_patch, *patch_source);
for (Vec2f &p : uv)
// Already a uv: no tiling scale, no aspect correction - see apply_uv_transform().
p = apply_uv_transform(p, layer, 1.f, /* normalized_uv */ true);
return uv;
}
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_vertex_uvs(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const
{
if (layer.projection_method == TextureProjectionMethod::ModelUV)
// Collapsed to one per vertex the same way the unwrap is - a vertex has one position, so the
// displacement can only read one height for it.
return collapse_corner_uvs(local_patch, model_uv_corners(local_patch, layer, patch_source));
// The bake maps the texture in world millimetres, so "Tile size (mm)" means the same thing on a
// scaled instance as it does on an untouched one (see build_texture_displacement()). Everything
// that has to agree with the bake - the fast preview's uvs, the checker/distortion overlays -
@@ -1283,8 +1317,11 @@ int GLGizmoTextureDisplacement::layer_projection_frame(const indexed_triangle_se
}
std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer) const
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const
{
if (layer.projection_method == TextureProjectionMethod::ModelUV)
return model_uv_corners(local_patch, layer, patch_source);
if (layer.projection_method == TextureProjectionMethod::LSCM) {
const indexed_triangle_set patch = patch_in_world(local_patch);
const float aspect = layer_texture_aspect(layer);
@@ -1294,7 +1331,7 @@ std::vector<Vec2f> GLGizmoTextureDisplacement::compute_layer_corner_uvs(const in
return uv;
}
// Single-valued per point: fan the per-vertex result out over the corners.
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer);
const std::vector<Vec2f> per_vertex = compute_layer_vertex_uvs(local_patch, layer, patch_source);
if (per_vertex.size() != local_patch.vertices.size())
return {};
std::vector<Vec2f> corner(local_patch.indices.size() * 3);
@@ -1315,7 +1352,11 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
// Uses the *live* selector (not the flushed model facet data), so this reflects an in-progress
// stroke immediately rather than only once it ends - the point of this preview mode is to be
// the fast, no-CPU-meshing one.
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
// The source map comes along because a ModelUV layer's uvs are read per base-mesh triangle; every
// other mapping ignores it.
std::vector<int> patch_source;
const indexed_triangle_set patch =
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
if (patch.indices.empty())
return;
@@ -1339,7 +1380,8 @@ void GLGizmoTextureDisplacement::rebuild_shaded_preview_mesh()
// Per *corner*, not per vertex: the mesh below is flat (unshared) anyway, so each triangle can
// carry its own chart's UVs - see compute_layer_corner_uvs().
const TextureDisplacementLayer *active = active_layer();
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active) : std::vector<Vec2f>{};
std::vector<Vec2f> corner_uv = active != nullptr ? compute_layer_corner_uvs(patch, *active, &patch_source) :
std::vector<Vec2f>{};
m_shaded_preview_uses_vertex_uv = corner_uv.size() == patch.indices.size() * 3;
if (!m_shaded_preview_uses_vertex_uv)
corner_uv.clear();
@@ -1996,7 +2038,9 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
const ModelVolume *mv = texture_volume();
if (mv == nullptr || m_triangle_selectors.empty())
return;
const indexed_triangle_set patch = m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER);
std::vector<int> patch_source; // only ModelUV reads it; see compute_layer_vertex_uvs()
const indexed_triangle_set patch =
m_triangle_selectors[0]->get_facets_strict(EnforcerBlockerType::ENFORCER, &patch_source);
if (patch.indices.empty())
return;
// Everything below works in the *patch's* vertex space, not the mesh's. Those agree only until a
@@ -2010,7 +2054,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
// The checker samples wherever the projection puts it; the projections the shader can't
// reconstruct (LSCM, ViewProjected) get a precomputed per-vertex uv, the rest project in-shader.
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer);
std::vector<Vec2f> uv = compute_layer_vertex_uvs(patch, *layer, &patch_source);
const bool have_uvs = uv.size() == patch.vertices.size();
m_uvcheck_uses_vertex_uv = have_uvs;
m_uvcheck_projection_mode = have_uvs ? 0 :
@@ -2019,7 +2063,7 @@ void GLGizmoTextureDisplacement::rebuild_uvcheck_mesh()
// different uv in each island it borders, so the shared-vertex form drew one triangle per face from
// a neighbouring island's placement. Only the *drawing* needs this; the distortion metric below is
// a per-vertex average by construction and keeps using `uv`.
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer);
const std::vector<Vec2f> corner_uv = compute_layer_corner_uvs(patch, *layer, &patch_source);
const bool have_corner_uvs = have_uvs && corner_uv.size() == patch.indices.size() * 3;
// Per-vertex area distortion in [0,1] (0.5 == ideal), only when both requested and possible.
@@ -2295,6 +2339,8 @@ void GLGizmoTextureDisplacement::queue_preview_job()
if (input.options.pipeline_v2)
input.options.v2_max_triangles_k = 0;
input.volume_to_world = texture_displacement_volume_to_world(*mv);
if (mv->has_texture_uv())
input.model_uv = mv->texture_uv;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
// Captured here rather than read in the handler: the palette is main-thread state, and the preview
@@ -3558,6 +3604,7 @@ const ModelVolume* GLGizmoTextureDisplacement::texture_volume() const
void GLGizmoTextureDisplacement::update_model_object()
{
store_panel_state();
bool updated = false;
ModelObject *mo = m_c->selection_info()->model_object();
int idx = -1;
@@ -3593,6 +3640,24 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
{
wxBusyCursor wait;
// Opening the gizmo puts the panel back where it was left: the layer and the mode come off the
// volume, because they are the user's place in this model's stack, and the dock state off the
// application config, because it is about their window. Only on open - this also runs on an
// undo/redo and on a layer switch, where overriding what the user is doing now would be wrong.
if (first_update) {
if (const ModelVolume *mv = texture_volume(); mv != nullptr) {
const TextureDisplacementOptions &opts = mv->texture_displacement_options;
m_panel_mode = std::clamp(opts.panel_mode, 0, 1);
// Only if that slot still holds a layer: the stack can have been edited by anything that
// does not go through the panel, and a selection pointing at nothing shows an empty panel.
const auto &stack = mv->texture_displacement_layers;
if (std::any_of(stack.begin(), stack.end(),
[&opts](const TextureDisplacementLayer &l) { return l.slot == opts.active_slot; }))
m_active_layer_slot = opts.active_slot;
}
m_undocked = wxGetApp().app_config->get_bool(UNDOCKED_CONFIG_KEY);
}
const ModelObject *mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear();
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
@@ -3641,6 +3706,14 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
rebuild_preview();
}
void GLGizmoTextureDisplacement::store_panel_state()
{
if (ModelVolume *mv = texture_volume(); mv != nullptr) {
mv->texture_displacement_options.active_slot = m_active_layer_slot;
mv->texture_displacement_options.panel_mode = m_panel_mode;
}
}
void GLGizmoTextureDisplacement::set_active_layer(int slot)
{
if (slot == m_active_layer_slot)
@@ -3649,6 +3722,7 @@ void GLGizmoTextureDisplacement::set_active_layer(int slot)
// reflect - otherwise they would be silently lost.
update_model_object();
m_active_layer_slot = slot;
store_panel_state();
update_from_model_object(false);
// The on-canvas gizmo (if on) is anchored to whichever layer is active - keep it in sync
// instead of leaving it pointing at the previous layer's (now stale) paint patch.
@@ -3676,10 +3750,13 @@ void GLGizmoTextureDisplacement::ensure_panel_icons()
// Paint / Erase, brush cursor shape, mapping, tiling, placement and the dock toggle.
"texture_displacement_add.svg", "texture_displacement_add_negative.svg", "circle_paint.svg",
"menu_obj_sphere.svg", "menu_obj_cube.svg", "menu_obj_cylinder.svg", "texture_displacement_map_unwrap.svg",
"texture_displacement_map_view.svg", "texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
"texture_displacement_map_view.svg", "texture_displacement_map_model_uv.svg",
"texture_displacement_tile_repeat.svg", "menu_mirror_x.svg",
"texture_displacement_adjust.svg", "canvas_drag.svg", "texture_displacement_move_up.svg",
"texture_displacement_move_down.svg", "texture_displacement_drag.svg",
"texture_displacement_select_all.svg", "texture_displacement_erase_all.svg",
// The "type a value instead" icon at the right end of every slider.
"edit.svg",
};
std::vector<std::string> paths;
paths.reserve(names.size());
@@ -3723,6 +3800,11 @@ void GLGizmoTextureDisplacement::add_texture_layer()
TextureDisplacementLayer layer;
layer.slot = free_slot;
// A model that came with a UV channel is mapped by it. Re-unwrapping such a model throws away the
// layout it was authored against and can only be worse than using it, so the default follows the
// model rather than the other way round; the Mapping row still offers every other method.
if (mv->has_texture_uv())
layer.projection_method = TextureProjectionMethod::ModelUV;
// Start the layer off on the first library texture rather than on nothing at all: a textureless
// layer looks broken (painting on it appears to do nothing, because there is no height map to
// displace by). The user swaps it for another from the layer's own picker.
@@ -4868,7 +4950,8 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams &params,
const std::vector<TextureDisplacementLayer> &layers, const std::vector<Vec2f> &model_uv,
const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
BakeStageRecorder *debug)
{
@@ -4895,6 +4978,13 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
bool had_paint = false;
for (const TriangleSelector::TriangleSplittingData &m : masks)
had_paint = had_paint || TriangleSelector::has_facets(m, EnforcerBlockerType::ENFORCER);
// The model's own UV channel for whatever `mesh` currently is. Every stage below keeps the input's
// surface - it evens out, refines or cuts the triangles lying on it - so each corner reads its uv
// straight off `base`'s channel at its own position instead of being carried stage by stage, which
// would compound each stage's approximation. Untouched while nothing has replaced the mesh yet.
const auto uv_now = [&]() {
return (model_uv.empty() || !changed) ? model_uv : transfer_uv_channel(base, model_uv, mesh.its);
};
if (!report(1))
return out;
@@ -4941,9 +5031,11 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// Feature-adaptive: sample the combined displacement so refinement follows texture
// curvature. A null sampler (only LSCM layers, or nothing decodable) falls back to the
// length baseline alone.
const std::vector<Vec2f> uv_stage = uv_now();
const std::vector<Vec2f> *uv_ptr = uv_stage.empty() ? nullptr : &uv_stage;
HeightFieldSampler sampler;
if (params.subdiv_feature)
sampler = make_combined_displacement_sampler(mesh.its, layers, current);
sampler = make_combined_displacement_sampler(mesh.its, layers, current, uv_ptr);
// Colour boundaries need triangles of their own - the chord test cannot see them, since
// the height field is perfectly smooth across a change of filament.
ColorFieldSampler color;
@@ -4951,7 +5043,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// boundaries are refined where the bake will actually change filament.
if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
make_palette_quantizer(color_settings.palette_pure));
make_palette_quantizer(color_settings.palette_pure), uv_ptr);
// The refinement follows perceived colour: a mix is one colour here, however the slicer
// interleaves its filaments layer by layer.
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
@@ -5010,8 +5102,10 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
std::vector<uint8_t> cut_region(mesh.its.indices.size(), 0);
for (size_t i = 0; i < cut_region.size() && i < source.size(); ++i)
cut_region[i] = (region[size_t(source[i])] & REFINE_PAINTED) ? 1 : 0;
const std::vector<Vec2f> uv_refined = uv_now();
const HeightFieldSampler refined_sampler =
make_combined_displacement_sampler(mesh.its, layers, current);
make_combined_displacement_sampler(mesh.its, layers, current,
uv_refined.empty() ? nullptr : &uv_refined);
std::vector<int> cut_source;
size_t cuts = 0;
indexed_triangle_set cut = cut_mesh_at_steps(mesh.its, cut_region, refined_sampler,
@@ -5035,6 +5129,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
if (!report(100) || !changed)
return out; // an empty result: nothing to commit, which is not a failure
out.uv = uv_now();
out.mesh = std::move(mesh.its);
out.masks = std::move(current);
return out;
@@ -5686,6 +5781,8 @@ void GLGizmoTextureDisplacement::queue_prepare(const TextureDisplacementPrepareP
input.layers = mv->texture_displacement_layers;
input.params = params;
input.snapshot_name = snapshot_name;
if (mv->has_texture_uv())
input.model_uv = mv->texture_uv;
if (params.subdiv_color_edge_mm > 0.f)
input.color = color_settings_for(*mv);
@@ -5919,8 +6016,8 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
return s + "...";
};
// Label column + a slider filling the rest of the row, value printed inside the track. Ctrl+click on
// any of these types a value in. `right_inset` keeps a layer card's inner padding.
// Label column + a slider filling the rest of the row, value printed inside the track.
// `right_inset` keeps a layer card's inner padding.
const auto slider_label = [&](const wxString &label) {
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
@@ -5928,17 +6025,107 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
};
// ---- Typed values on sliders ----
// Every slider in the panel goes through typed_slider(), so all of them take a number the same
// two ways: Ctrl+click on the track, or the edit icon at the right end for the same box without
// the modifier. Both end up in ImGui's own "temp input" mode - the icon only asks ImGui to focus
// the slider, which is what turns it into an input box (see SliderScalar) - so the parsing,
// the clamping and the Enter/click-away behaviour are ImGui's, not a second implementation.
// Retires a click whose row is no longer being drawn - the panel changed mode in between - so that
// it cannot fire later when that row comes back. Before any row is drawn, so a live one still wins.
if (!m_slider_type_request.empty() && ImGui::GetFrameCount() > m_slider_type_request_frame + 1)
m_slider_type_request.clear();
// Exactly as tall as the slider it sits beside, so it lines up with the track without the row
// having to be re-centred around it; icon_button() pads the glyph itself down to about 0.7 of this.
const float edit_icon_w = frame_h;
// What the slider's own width has to give up for the icon beside it.
const float edit_icon_room = edit_icon_w + gap_s;
// One end of a range, through the slider's own format, so the bounds in the tooltip read exactly
// like the value in the track. A parenthesised note in the format - "(auto)" - is dropped: it
// describes the current state, not the bound.
const auto fmt_bound = [](const char *format, auto v) {
char buf[64];
std::snprintf(buf, sizeof(buf), format, v);
std::string out(buf);
const size_t paren = out.find('(');
if (paren != std::string::npos)
out.erase(paren);
while (!out.empty() && out.back() == ' ')
out.pop_back();
return out;
};
const auto range_text = [](const std::string &lo, const std::string &hi) {
return Slic3r::format(_u8L("Range: %1% to %2%"), lo, hi);
};
const auto edit_icon_tip = [&](const std::string &range) {
return _u8L("Type a value instead of dragging. Ctrl+click on the slider does the same.") + "\n\n" + range;
};
// Draws `slider` at `item_w` (SetNextItemWidth's convention: negative measures back from the right
// edge, and the caller has already left edit_icon_room in it), then the edit icon. `tip` and the
// range go on both. Returns whatever the slider returned.
const auto typed_row = [&](const char *id, float item_w, const wxString &tip, const std::string &range,
const std::function<bool()> &slider) -> bool {
// Deferred from the click a frame ago: SetKeyboardFocusHere() is a request for the *next*
// frame, so it has to be issued before the slider it names, not from the icon beside it.
if (m_slider_type_request == id) {
ImGui::SetKeyboardFocusHere();
m_slider_type_request.clear();
m_parent.set_as_dirty(); // the focus only lands on a frame that actually gets drawn
}
ImGui::SetNextItemWidth(item_w);
const bool changed = slider();
// AllowWhenDisabled: several of these rows are drawn inside disabled_begin(), and a greyed
// slider is exactly when the user needs to be told what it is and why it is not moving.
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
wxString full = tip;
if (!full.IsEmpty())
full += "\n\n";
m_imgui->tooltip(full + from_u8(range), wrap_w);
}
// SameLine() puts the cursor back at the slider's own top (ItemSize records the Y the item was
// drawn at), so an icon of the same height lands on the track wherever the row placed it.
ImGui::SameLine(0.f, gap_s);
if (icon_button(int(ImGui::GetID(id)), "edit.svg", edit_icon_w, _L("Edit"), from_u8(edit_icon_tip(range)))) {
m_slider_type_request = id;
m_slider_type_request_frame = ImGui::GetFrameCount();
m_parent.set_as_dirty(); // the next frame is where the request above is turned into focus
}
return changed;
};
// The two forms every slider in the panel is one of. They derive the range from the bounds they are
// already given, so no row states its bounds or its format twice and the tooltip cannot drift from
// the track. `item_w` is as typed_row() takes it.
const auto typed_slider = [&](const char *id, float item_w, float *v, float v_min, float v_max, const char *format,
bool log, const wxString &tip) -> bool {
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderFloat(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
});
};
const auto typed_slider_int = [&](const char *id, float item_w, int *v, int v_min, int v_max, const char *format,
bool log, const wxString &tip) -> bool {
return typed_row(id, item_w, tip, range_text(fmt_bound(format, v_min), fmt_bound(format, v_max)), [&] {
return ImGui::SliderInt(id, v, v_min, v_max, format,
ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
});
};
// Label column + the slider filling the rest of the row.
const auto float_row = [&](const char *id, const wxString &label, float *v, float v_min, float v_max, const char *format,
bool log, float right_inset) -> bool {
bool log, float right_inset, const wxString &tip) -> bool {
slider_label(label);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderFloat(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp | (log ? ImGuiSliderFlags_Logarithmic : 0));
return typed_slider(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, log, tip);
};
const auto int_row = [&](const char *id, const wxString &label, int *v, int v_min, int v_max, const char *format,
float right_inset) -> bool {
float right_inset, const wxString &tip) -> bool {
slider_label(label);
ImGui::SetNextItemWidth(-std::max(right_inset, 1.f));
return ImGui::SliderInt(id, v, v_min, v_max, format, ImGuiSliderFlags_AlwaysClamp);
return typed_slider_int(id, -(std::max(right_inset, 1.f) + edit_icon_room), v, v_min, v_max, format, false, tip);
};
const auto layer_name = [](const TextureDisplacementLayer &l) {
return l.name.empty() ? Slic3r::format(_u8L("Layer %1%"), l.slot + 1) : l.name;
@@ -5986,6 +6173,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"not what you wanted and you know why."));
if (clicked && !on) {
m_panel_mode = m;
store_panel_state();
if (!pro_mode()) {
// Leaving the subdivision preview open would strand a wireframe whose controls just
// disappeared, so close it as part of the switch.
@@ -6008,6 +6196,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Detach this panel so it can be dragged anywhere over the 3D view, or dock it back beside "
"the toolbar.")))
m_undocked = !m_undocked;
wxGetApp().app_config->set_bool(UNDOCKED_CONFIG_KEY, m_undocked);
}
ImGui::Separator();
@@ -6160,11 +6349,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
const auto centre_on_row = [&](float h) { ImGui::SetCursorPosY(row_y + std::round((icon_md - h) * 0.5f)); };
vsep(icon_md);
if (is_brush_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm)));
centre_on_row(frame_h);
ImGui::SliderFloat("##cursor_radius", &m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm",
ImGuiSliderFlags_AlwaysClamp);
hover_tip(m_desc.at("cursor_size"));
typed_slider("##cursor_radius",
std::max(1.f, row_end - ImGui::GetCursorPosX() - (3.f * gap_s + 1.f + 2.f * icon_sm) - edit_icon_room),
&m_cursor_radius, CursorRadiusMin, CursorRadiusMax, "%.2f mm", false, m_desc.at("cursor_size"));
vsep(icon_sm);
const bool is_circle = m_cursor_type == TriangleSelector::CursorType::CIRCLE;
centre_on_row(icon_sm);
@@ -6177,11 +6365,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
_L("Sphere - paints only within a ball around the point under the cursor")))
m_cursor_type = TriangleSelector::CursorType::SPHERE;
} else if (is_area_mode) {
ImGui::SetNextItemWidth(std::max(1.f, row_end - ImGui::GetCursorPosX()));
centre_on_row(frame_h);
ImGui::SliderFloat("##smart_fill_angle", &m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°",
ImGuiSliderFlags_AlwaysClamp);
hover_tip(_u8L("Angle threshold - the fill stops at edges sharper than this"));
typed_slider("##smart_fill_angle", std::max(1.f, row_end - ImGui::GetCursorPosX() - edit_icon_room),
&m_smart_fill_angle, SmartFillAngleMin, SmartFillAngleMax, "%.0f°", false,
_L("Angle threshold - the fill stops at edges sharper than this"));
} else {
centre_on_row(frame_h);
ImGui::AlignTextToFramePadding();
@@ -6451,16 +6638,25 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// The three controls nearly every layer needs; everything else waits behind "More settings".
// Depth and tile size are logarithmic, so both ends of their wide ranges stay usable.
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad);
hover_tip(_u8L("Height of the relief: how far white in the texture lifts the surface, in "
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad);
hover_tip(_u8L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
"more often and makes its detail finer; with Tile off, this is the size of the "
"single copy."));
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad);
hover_tip(_u8L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
"edge of the model."));
m_preview_params_dirty |= float_row("##depth", _L("Depth"), &layer.depth_mm, 0.01f, 10.f, "%.3f mm", true, card_pad,
_L("Height of the relief: how far white in the texture lifts the surface, in "
"millimetres. Black does not move it at all, unless Midlevel says otherwise."));
// Mapped by the model's own UV layout there is no millimetre scale to set: the layout is the
// placement (see TextureProjectionMethod::ModelUV), so the slider would move nothing. The row
// states why rather than hiding: float_row()'s tooltip is shown for a disabled slider too.
const bool tile_size_na = layer.projection_method == TextureProjectionMethod::ModelUV;
m_imgui->disabled_begin(tile_size_na);
m_preview_params_dirty |= float_row("##tile_size", _L("Tile size"), &layer.tiling_scale, 0.2f, 200.f, "%.2f mm", true, card_pad,
tile_size_na ?
_L("Mapped by the model's own UV layout, which is already the placement - one "
"copy of the texture covers it exactly, so there is no tile size to set.") :
_L("How wide one copy of the texture is on the model. Smaller repeats the pattern "
"more often and makes its detail finer; with Tile off, this is the size of the "
"single copy."));
m_imgui->disabled_end();
m_preview_params_dirty |= float_row("##rotation", _L("Rotation"), &layer.rotation_deg, 0.f, 360.f, "%.0f°", false, card_pad,
_L("Turns the texture on the surface, in degrees - for lining a pattern up with an "
"edge of the model."));
if (m_layer_expanded[size_t(layer.slot)]) {
{
@@ -6475,22 +6671,22 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Midlevel: the height that means "don't move". At 0 the surface only ever bulges outwards;
// at 0.5 mid-grey is neutral and darker texels cut inwards.
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad);
hover_tip(_u8L("Which gray stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
"still push out - one image both embosses and engraves.\n\n"
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
"cut can pass through the other side."));
m_preview_params_dirty |= float_row("##midlevel", _L("Midlevel"), &layer.midlevel, 0.f, 10.f, "%.2f", false, card_pad,
_L("Which gray stays where the surface already is. At 0 the texture only pushes "
"outwards; at 0.5 mid-gray stays put, so darker grays cut in and lighter ones "
"still push out - one image both embosses and engraves.\n\n"
"What cuts in has to fit: inside a sharp corner or through a thin wall, a deep "
"cut can pass through the other side."));
if (layer.midlevel > 0.f && layer.depth_mm > 1.f) {
ImGui::PushTextWrapPos(content_rx - card_pad);
m_imgui->warning_text(_L("Deep inward displacement may self-intersect."));
ImGui::PopTextWrapPos();
}
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad);
hover_tip(_u8L("Blurs the image before it is used, which rounds off hard steps and removes "
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
"costs fine detail."));
m_preview_params_dirty |= float_row("##smoothing", _L("Smoothing"), &layer.smoothing, 0.f, 1.f, "%.2f", false, card_pad,
_L("Blurs the image before it is used, which rounds off hard steps and removes "
"speckle from a noisy photo. Raise it if the relief looks harsh or grainy; it "
"costs fine detail."));
// Edge fade: the relief flattens toward the boundary of the painted area.
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Edge fade") + "##edge_smoothing", layer.edge_smoothing);
@@ -6498,12 +6694,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"blends into the bare surface instead of stopping at a step."));
ImGui::SameLine();
m_imgui->disabled_begin(!layer.edge_smoothing);
ImGui::SetNextItemWidth(-card_pad);
m_preview_params_dirty |= ImGui::SliderFloat("##edge_amount", &layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f",
ImGuiSliderFlags_AlwaysClamp);
m_preview_params_dirty |= typed_slider("##edge_amount", -(card_pad + edit_icon_room),
&layer.edge_smoothing_amount, 0.02f, 1.f, "%.2f", false,
_L("How far in the fade reaches, as a share of the painted area. Small "
"values soften a narrow band at the edge; 1 flattens almost all of it."));
m_imgui->disabled_end();
hover_tip(_u8L("How far in the fade reaches, as a share of the painted area. Small values "
"soften a narrow band at the edge; 1 flattens almost all of it."));
// Invert and Colours share a row.
{
@@ -6548,23 +6743,23 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// Every mix can become a filament slot, and there are only so many beside the
// physical filaments.
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad,
_L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created.")))
m_preview_params_dirty = true;
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created."));
// After the slider, so a change shows in the same frame.
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("Cleans up single stray triangles of the wrong color, which detail finer "
if (int_row("##color_despeckle", _L("Denoise"), &opts.color_despeckle, 0, 6, "%d", card_pad,
_L("Cleans up single stray triangles of the wrong color, which detail finer "
"than the mesh leaves behind. Raise it if the result looks speckled, "
"lower it if small features are being swallowed."));
"lower it if small features are being swallowed.")))
m_preview_params_dirty = true;
}
}
// Mapping, as five icons in TextureProjectionMethod's own order.
// Mapping, as one icon per TextureProjectionMethod, in the enum's own order.
{
const float x0 = ImGui::GetCursorPosX();
ImGui::AlignTextToFramePadding();
@@ -6592,13 +6787,26 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
{ "texture_displacement_map_view.svg", _L("From view"),
_L("From view - projects straight onto the painted area from where you are looking, like a slide "
"projector.") },
{ "texture_displacement_map_model_uv.svg", _L("Model UV"),
_L("Model UV - uses the texture coordinates the model itself came with, so a texture made for "
"that model lands exactly where it was painted. Nothing is unwrapped, and Tile size does "
"not apply: the model's own layout is the placement.") },
};
// Only a volume that actually carries a UV channel can be mapped by it. Shown
// greyed out rather than hidden, so the reason is visible instead of the control
// simply being missing.
const wxString model_uv_na =
(mv != nullptr && mv->has_texture_uv()) ?
wxString() :
_L("This model carries no texture coordinates of its own. Only a model imported with a UV "
"channel - a textured OBJ, say - can be mapped by them.");
for (int mi = 0; mi < int(IM_ARRAYSIZE(mappings)); ++mi) {
if (mi > 0)
ImGui::SameLine(0.f, gap_s);
const auto method = static_cast<TextureProjectionMethod>(mi);
const wxString na = (method == TextureProjectionMethod::ModelUV) ? model_uv_na : wxString();
if (icon_toggle(830 + mi, mappings[mi].file, layer.projection_method == method, icon_sm, mappings[mi].label,
mappings[mi].tip) &&
mappings[mi].tip, na) &&
layer.projection_method != method) {
// Capture the current view the moment "From view" is chosen, so it does something
// sensible immediately rather than projecting from a stale/default direction.
@@ -6665,10 +6873,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"gets the texture, and its border becomes the edge of the projection."));
if (projector_open) {
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad))
if (int_row("##projector_opacity", _L("Opacity"), &m_projector_opacity, 20, 255, "%d", card_pad,
_L("How solid the frame window looks while you place it. Lower to see the "
"model through it; it does not affect the result.")))
m_projector_frame->set_opacity(m_projector_opacity);
hover_tip(_u8L("How solid the frame window looks while you place it. Lower to see the "
"model through it; it does not affect the result."));
if (m_imgui->button(_u8L("Apply projection frame"))) {
const int painted_count = apply_projection_frame();
@@ -6728,18 +6936,33 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
const std::string blend_add = _u8L_CONTEXT("Add", "Texture Displacement");
const std::string blend_override = _u8L_CONTEXT("Override", "Texture Displacement");
const std::string blend_subtract = _u8L_CONTEXT("Subtract", "Texture Displacement");
const std::string blend_multiply = _u8L_CONTEXT("Multiply", "Texture Displacement");
const std::string blend_divide = _u8L("Divide");
const char *blend_items[] = { blend_add.c_str(), blend_subtract.c_str(), blend_multiply.c_str(), blend_divide.c_str() };
const char *blend_items[] = { blend_add.c_str(), blend_override.c_str(), blend_subtract.c_str(),
blend_multiply.c_str(), blend_divide.c_str() };
// The combo's order is the panel's, not the enum's - Override reads as the sibling of Add and
// belongs next to it, while TextureBlendMode's numbering is fixed by what projects already
// store. Hence the explicit mapping rather than a cast of the index.
static const TextureBlendMode blend_modes[] = { TextureBlendMode::Add, TextureBlendMode::Override,
TextureBlendMode::Subtract, TextureBlendMode::Multiply,
TextureBlendMode::Divide };
static_assert(IM_ARRAYSIZE(blend_items) == IM_ARRAYSIZE(blend_modes),
"every Blend entry needs its mode");
// The first layer has nothing before it to combine with - build_texture_displacement() makes it
// add regardless - so its combo shows Add and cannot be changed.
const bool base_layer = li == 0;
int blend_mode = base_layer ? 0 : static_cast<int>(layer.blend_mode);
int blend_mode = 0;
if (!base_layer)
for (int bi = 0; bi < int(IM_ARRAYSIZE(blend_modes)); ++bi)
if (blend_modes[bi] == layer.blend_mode)
blend_mode = bi;
ImGui::SetNextItemWidth(-(card_pad + gap_s + icon_sm));
m_imgui->disabled_begin(base_layer);
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer) {
layer.blend_mode = static_cast<TextureBlendMode>(blend_mode);
if (scoped_combo("##blend_mode", &blend_mode, blend_items, IM_ARRAYSIZE(blend_items)) && !base_layer &&
blend_mode >= 0 && blend_mode < int(IM_ARRAYSIZE(blend_modes))) {
layer.blend_mode = blend_modes[blend_mode];
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
@@ -6747,10 +6970,11 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->tooltip(base_layer ?
_u8L("The base layer has nothing beneath it to combine with, so it always adds.") :
_u8L("What this layer does where it overlaps the ones below. Add and "
"Subtract pile relief on or carve it away. Multiply and Divide scale "
"what is underneath, which turns this layer into a mask over it - "
"there, Depth acts as a strength, and 1 mm leaves white areas "
"untouched."),
"Subtract pile relief on or carve it away. Override replaces them, "
"so only this layer's relief is left where it is painted. Multiply "
"and Divide scale what is underneath, which turns this layer into a "
"mask over it - there, Depth acts as a strength, and 1 mm leaves "
"white areas untouched."),
wrap_w);
ImGui::SameLine(0.f, gap_s);
@@ -6969,57 +7193,57 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
// pattern can sample four points that all happen to land at similar heights, report no error,
// and stall before refinement ever starts.
if (float_row("##subdiv_target", m_subdivide_feature ? _L("Max edge") : _L("Target edge"), &m_subdivide_target_mm,
0.001f, 20.f, "%.3f mm", true, 0.f))
0.001f, 20.f, "%.3f mm", true, 0.f,
m_subdivide_feature ?
_L("No triangle in the painted area stays larger than this, even where the texture "
"is flat. Keep it near the size of the smallest feature you want to come out.") :
_L("Triangles in the painted area are split until none is larger than this. Smaller "
"means finer detail and, fast, a lot more triangles.")))
preview_live();
hover_tip(m_subdivide_feature ?
_u8L("No triangle in the painted area stays larger than this, even where the texture "
"is flat. Keep it near the size of the smallest feature you want to come out.") :
_u8L("Triangles in the painted area are split until none is larger than this. Smaller "
"means finer detail and, fast, a lot more triangles."));
if (m_subdivide_feature) {
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f))
if (float_row("##subdiv_detail", _L("Detail"), &m_subdivide_detail_mm, 0.001f, 1.f, "%.3f mm", true, 0.f,
_L("How far the mesh may sit from the shape the texture describes, in millimetres. "
"Smaller follows fine detail and costs triangles; larger only chases the big "
"features.")))
preview_live();
hover_tip(_u8L("How far the mesh may sit from the shape the texture describes, in millimetres. "
"Smaller follows fine detail and costs triangles; larger only chases the big "
"features."));
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f))
if (float_row("##subdiv_min", _L("Min edge"), &m_subdivide_min_edge_mm, 0.001f, 20.f, "%.3f mm", true, 0.f,
_L("A floor on triangle size, so a sharp step in the texture cannot be chased "
"forever. Lower it for finer relief, raise it if the count runs away at hard "
"edges.")))
preview_live();
hover_tip(_u8L("A floor on triangle size, so a sharp step in the texture cannot be chased "
"forever. Lower it for finer relief, raise it if the count runs away at hard "
"edges."));
}
// Applies in both adaptive sub-modes: it is about the step the bake puts at the paint's edge, which
// exists whether or not "Follow texture detail" is on. 0 turns the band off.
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
if (float_row("##subdiv_border", _L("Edge detail"), &m_subdivide_border_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
_L("Triangle size along the outline of the painted area, where the relief drops back to the "
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
"outline. 0 turns it off.")))
preview_live();
hover_tip(_u8L("Triangle size along the outline of the painted area, where the relief drops back to the "
"bare surface. Lower it if that rim looks jagged; it only costs triangles along the "
"outline. 0 turns it off."));
// Only worth showing when a layer is actually colouring: with no colour there is no boundary for it
// to refine and the control would do nothing whatever it is set to.
if (mv != nullptr && any_layer_colors(*mv)) {
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f))
if (float_row("##subdiv_color", _L("Color detail"), &m_subdivide_color_mm, 0.f, 5.f, "%.3f mm", false, 0.f,
_L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
"color edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of color. 0 turns it off.")))
preview_live();
hover_tip(_u8L("Triangle size where two colors meet. Each triangle prints in one filament, so a "
"color edge can only be as sharp as the triangles along it - and nothing else "
"refines there, since the surface is flat across a change of color. 0 turns it off."));
}
ImGui::TextDisabled("%s", _u8L("Triangle budget: set with Triangles, below.").c_str());
if (m_subdivide_editing && m_subdivide_preview_tris > 0)
m_imgui->text(Slic3r::format(_u8L("Preview: %1% triangles"), m_subdivide_preview_tris));
} else {
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f)) {
if (int_row("##subdiv_steps", _L("Steps"), &m_subdivide_count, 0, 5, "%d", 0.f,
_L("Splits every triangle of the model into four, this many times over. Each step "
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
"only the painted area unless you need the whole model finer."))) {
if (m_subdivide_editing)
rebuild_subdivide_preview(); // a count of 0 clears the preview, it doesn't compute one
m_parent.set_as_dirty();
}
hover_tip(_u8L("Splits every triangle of the model into four, this many times over. Each step "
"quadruples the count - 3 turns 100 k triangles into 6.4 M - so prefer refining "
"only the painted area unless you need the whole model finer."));
}
{
@@ -7081,9 +7305,9 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
}
m_remesh_target_edge_mm = cnt > 0 ? std::clamp(float(sum / double(cnt)), 0.1f, 20.f) : 1.f;
}
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f);
hover_tip(_u8L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
"even mesh; this is what makes one out of an uneven import."));
float_row("##remesh_edge", _L("Target edge"), &m_remesh_target_edge_mm, 0.1f, 20.f, "%.2f mm", true, 0.f,
_L("Triangle size the whole model is rebuilt with, in millimetres. Displacement works best on an "
"even mesh; this is what makes one out of an uneven import."));
m_imgui->bbl_checkbox(_L("Keep sharp edges") + "##remesh_sharp", m_remesh_keep_sharp_edges);
hover_tip(_u8L("Holds hard edges and open borders in place while the rest is remeshed. Without it "
@@ -7091,11 +7315,10 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"a cube comes back with wobbly edges."));
ImGui::SameLine();
m_imgui->disabled_begin(!m_remesh_keep_sharp_edges);
ImGui::SetNextItemWidth(-1.f);
ImGui::SliderFloat("##remesh_sharp_angle", &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", ImGuiSliderFlags_AlwaysClamp);
m_imgui->disabled_end();
hover_tip(_u8L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
typed_slider("##remesh_sharp_angle", -(1.f + edit_icon_room), &m_remesh_sharp_angle_deg, 5.f, 90.f, "%.0f°", false,
_L("How sharp a fold has to be, in degrees, before the remesher treats it as an edge worth "
"keeping. Lower protects more of the model's shape; higher rebuilds more of it evenly."));
m_imgui->disabled_end();
m_imgui->disabled_begin(mv == nullptr || busy);
if (ImGui::Button(_u8L("Remesh").c_str()))
@@ -7123,16 +7346,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"on a layer is a different thing: it blurs the image before it is used."));
if (opts.smooth_enabled) {
float percent = opts.smooth_strength * 100.f;
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f)) {
if (float_row("##dispsmooth", _L_CONTEXT("Strength", "Texture Displacement"), &percent, 1.f, 100.f, "%.0f %%", false, 0.f,
_L("How far each pass pulls a vertex towards its neighbours. High values round the "
"relief off quickly; low values need more passes but keep more of the detail."))) {
opts.smooth_strength = std::clamp(percent / 100.f, 0.01f, 1.f);
m_preview_params_dirty = true;
}
hover_tip(_u8L("How far each pass pulls a vertex towards its neighbours. High values round the "
"relief off quickly; low values need more passes but keep more of the detail."));
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f))
if (int_row("##dispsmoothit", _L("Passes"), &opts.smooth_iterations, 1, 10, "%d", 0.f,
_L("How many smoothing passes to run. More passes spread the smoothing further across "
"the surface; Strength decides how much each one moves.")))
m_preview_params_dirty = true;
hover_tip(_u8L("How many smoothing passes to run. More passes spread the smoothing further across "
"the surface; Strength decides how much each one moves."));
m_preview_params_dirty |= m_imgui->bbl_checkbox(_L("Ignore outer ring"), opts.smooth_skip_border);
hover_tip(_u8L("Keeps the outer ring of the painted area out of the smoothing. Its neighbours "
@@ -7175,16 +7398,16 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
int shown_k = auto_budget ? v2_recommendation(*mv).budget_k : opts.v2_max_triangles_k;
m_imgui->disabled_begin(auto_budget);
// TRN Slider value: %d is the triangle budget in thousands
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f)) {
if (int_row("##v2budget", _L("Budget"), &shown_k, 0, 4000, auto_budget ? _u8L("%d k (auto)").c_str() : "%d k", 0.f,
_L("How many thousand triangles this bake may spend on the area you painted. "
"Relief already baked into the rest of the model is kept on top of it, so a "
"second bake somewhere else gets the same budget as the first. Raise it if the "
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
"the refinement produced, however many that is."))) {
opts.v2_max_triangles_k = shown_k;
m_preview_params_dirty = true;
}
m_imgui->disabled_end();
hover_tip(_u8L("How many thousand triangles this bake may spend on the area you painted. "
"Relief already baked into the rest of the model is kept on top of it, so a "
"second bake somewhere else gets the same budget as the first. Raise it if the "
"warning below Resolution says the detail will not fit; 0 keeps every triangle "
"the refinement produced, however many that is."));
}
if (SHOW_PIPELINE_DEV_CONTROLS && opts.pipeline_v2) {
// Keeping the relief above the plate is not a checkbox: it is unconditional, in both pipelines
@@ -7291,26 +7514,28 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"default. Untick to set both yourself - worth doing for a texture much finer or "
"much coarser than the part it sits on."));
ImGui::SameLine();
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
float shown = auto_res ? rec.edge_mm : opts.v2_refine_mm;
// Held in a local: the format outlives the call that reads it, and the slider and the
// range in its tooltip are then formatted the same way.
const std::string v2edge_format = auto_res ? _u8L("%.2f mm (auto)") : std::string("%.2f mm");
const wxString v2edge_tip =
auto_res && rec.edge_mm > 0.f ?
from_u8(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
"budget %2% k. Untick Auto to set them yourself."),
rec.edge_mm, rec.budget_k)) :
_L("How fine the mesh is made under the paint, in millimetres. It has to be "
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
"four times as many, and once they no longer fit the budget the bake simplifies "
"back down and the detail goes with it.");
m_imgui->disabled_begin(busy || auto_res);
if (ImGui::SliderFloat("##v2edge", &shown, 0.02f, 2.f, auto_res ? _u8L("%.2f mm (auto)").c_str() : "%.2f mm",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
if (typed_slider("##v2edge", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room, &shown, 0.02f, 2.f,
v2edge_format.c_str(), true, v2edge_tip)) {
opts.v2_refine_mm = shown;
m_preview_params_dirty = true;
m_parent.set_as_dirty();
}
m_imgui->disabled_end();
if (auto_res && rec.edge_mm > 0.f)
hover_tip(Slic3r::format(_u8L("Chosen from the size of the model: %1$.2f mm triangles, "
"budget %2% k. Untick Auto to set them yourself."),
rec.edge_mm, rec.budget_k));
else
hover_tip(_u8L("How fine the mesh is made under the paint, in millimetres. It has to be "
"smaller than the detail you want out of the texture - a 0.5 mm groove needs "
"triangles well under 0.5 mm. Smaller costs triangles fast: halving it needs "
"four times as many, and once they no longer fit the budget the bake simplifies "
"back down and the detail goes with it."));
// What this resolution costs over what is painted, against what the budget allows. Refining
// past the budget is not an error - the bake simplifies back down to it - but the result
@@ -7342,18 +7567,17 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
m_imgui->text(_L("Triangles"));
ImGui::SameLine();
ImGui::SetCursorPosX(std::max(ImGui::GetCursorPosX(), x0 + label_w));
ImGui::SetNextItemWidth(x0 + panel_w - ImGui::GetCursorPosX());
m_imgui->disabled_begin(busy);
if (ImGui::SliderInt("##subdivbudget", &m_subdivide_budget_k, 10, 2000, "+%d k",
ImGuiSliderFlags_AlwaysClamp | ImGuiSliderFlags_Logarithmic)) {
if (typed_slider_int("##subdivbudget", x0 + panel_w - ImGui::GetCursorPosX() - edit_icon_room,
&m_subdivide_budget_k, 10, 2000, "+%d k", true,
_L("How many thousand triangles the refinement may add to the model. The triangle "
"that fits worst is always split first, so even a run that spends the lot has "
"spent it where it shows most. Raise it if the relief still looks coarse."))) {
if (m_subdivide_editing)
rebuild_subdivide_preview();
m_parent.set_as_dirty();
}
m_imgui->disabled_end();
hover_tip(_u8L("How many thousand triangles the refinement may add to the model. The triangle "
"that fits worst is always split first, so even a run that spends the lot has "
"spent it where it shows most. Raise it if the relief still looks coarse."));
}
const float button_h = std::round(frame_h * 1.25f);
@@ -7372,7 +7596,7 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
: m_tool_type == ToolType::SMART_FILL ? m_shortcuts_bucket_fill
: m_tool_type == ToolType::BRUSH ? m_shortcuts_brush
: m_tool_type == ToolType::GAP_FILL ? m_shortcuts_gap_fill
: std::vector<std::pair<wxString, wxString>>{};
: m_shortcuts_sliders; // the sliders are there whether or not a painting tool is
ImGui::SetCursorPosY(row_y + (row_h - icon_h) * .5f); // center vertically
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(int(m_imgui->scaled(.5f)), style.ItemSpacing.y));
@@ -61,6 +61,12 @@ public:
static TextureDisplacementPrepareResult prepare_mesh(const indexed_triangle_set &base,
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
// `base`'s own UV channel, when it has one
// (ModelVolume::texture_uv). Resampled onto
// the result and reported back in it, and fed
// to the samplers so a ModelUV layer drives
// the refinement through its own layout.
const std::vector<Vec2f> &model_uv,
const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings,
const DisplacementProgressFn &progress,
@@ -262,6 +268,9 @@ private:
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually
// changed something, so the caller can invalidate the preview.
bool apply_standard_mode_presets(ModelVolume *mv);
// Copies the panel's own state (active layer, Standard/Pro) onto the volume, so the project file
// can carry it. Cheap and idempotent - called wherever either of the two changes.
void store_panel_state();
// 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
@@ -404,6 +413,12 @@ private:
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation.
// Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{};
// The slider whose edit icon was just clicked, as its ImGui id, and the frame it was clicked on.
// The row acts on it on the next frame - ImGui's keyboard-focus request only takes effect a frame
// later - and clears it; the frame number is what retires a request whose row has gone off screen
// in the meantime. Panel state only, not saved with the project.
std::string m_slider_type_request;
int m_slider_type_request_frame = 0;
// "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
@@ -818,18 +833,31 @@ private:
// 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 shaded preview and the UV-check overlay.
// unwrap), ModelUV (the model's own channel) 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 shaded
// preview and the UV-check overlay.
//
// `patch_source` maps each patch triangle back to its base-mesh triangle
// (TriangleSelector::get_facets_strict()'s out_source). Only ModelUV needs it - its channel is per
// base triangle - and without it a ModelUV layer reports no uvs and the preview falls back.
std::vector<Vec2f> compute_layer_vertex_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer) const;
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source = nullptr) const;
// The same, but three UVs per patch triangle (corner 0..2 of triangle i at 3i..3i+2). This is what
// the flat, unshared-vertex preview meshes actually want: under LSCM a seam vertex has a different
// UV in each island it borders, so collapsing to one per vertex handed a triangle at an unjoined
// seam its neighbour's placement - one visibly skewed triangle per face. Every other projection is
// single-valued per point, so there a corner's UV is just its vertex's.
std::vector<Vec2f> compute_layer_corner_uvs(const indexed_triangle_set &patch,
const TextureDisplacementLayer &layer) const;
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source = nullptr) const;
// The model's own channel over `local_patch`, already through apply_uv_transform() - three per
// patch triangle. Empty when the volume has no channel, or when `patch_source` is missing. Shared
// by the two functions above so the per-vertex and per-corner forms can never disagree.
std::vector<Vec2f> model_uv_corners(const indexed_triangle_set &local_patch,
const TextureDisplacementLayer &layer,
const std::vector<int> *patch_source) const;
// The in-shader projection for `layer` (0 Triplanar, 1 Cylindrical, 2 Spherical) plus, for the two
// wrapping ones, the patch centroid and cylinder axis they wrap around - in the texture frame the
// shaders project in. Taken from the bake's own texture_displacement_patch_frame(), so a preview
@@ -971,9 +999,12 @@ private:
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.
// grab), instead of being pinned to the right of the gizmo toolbar. Kept in the application
// config rather than in the project: it describes the user's window, not the model, so opening
// someone else's project must not move their panel.
bool m_undocked = false;
// The config key behind m_undocked.
static constexpr const char *UNDOCKED_CONFIG_KEY = "texture_displacement_panel_undocked";
// Smooth scrolling for the panel body (everything between the header and the pinned Bake footer).
// ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
@@ -1016,6 +1047,9 @@ private:
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill;
// Contains all shortcuts in the format of {shortcut, description}, e.g. {alt + _L("Left mouse button"), _L("Part_selection")}
std::vector<std::pair<wxString, wxString>> m_shortcuts_gap_fill;
// The two ways every slider in the panel takes a typed value. They hold whatever tool is selected,
// so they are tacked onto each of the lists above and stand on their own when no tool is active.
std::vector<std::pair<wxString, wxString>> m_shortcuts_sliders;
// Icons for the panel's icon buttons (tools, views, mapping, tiling, layer actions). Loaded through IconManager with
// the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
@@ -8,6 +8,7 @@
#include "libslic3r/TextureDisplacement.hpp"
#include <exception>
#include <cstddef>
#include <cstdint>
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory>
@@ -26,6 +27,7 @@
#include "slic3r/Utils/UndoRedo.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include <array>
namespace Slic3r::GUI {
@@ -74,7 +76,8 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
}
return true;
},
color, m_input.volume_to_world, nullptr, &m_stats));
color, m_input.volume_to_world, nullptr, &m_stats,
m_input.model_uv.empty() ? nullptr : &m_input.model_uv));
// 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.
@@ -126,16 +129,71 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
{ mix.a_percent(), 100 - mix.a_percent() });
});
// Whether the bake replaced the triangles or only moved the vertices it was given. The classic
// path is topology-preserving by contract, so every per-triangle annotation on the volume still
// means what it did; the default pipeline refines, cuts and simplifies, so none of them do.
// Both are checked: the flag is what the bake promised, the count is what it delivered. Only
// the triangles matter - the annotations are per facet and reference no vertex index - so a
// vertex count that changed on its own (the bake compacts unreferenced vertices away) is not a
// reason to remap anything.
const bool topology_changed = m_input.options.pipeline_v2 ||
m_result.its.indices.size() != m_input.base_mesh.indices.size();
// Carry the volume's existing paint - the model's own colours above all - across that rebuild.
// Without it a bake left the mmu_segmentation_facets bitstream pointing at a triangle set that
// no longer existed, which is what scrambled the colours of an imported coloured model even
// with colour switched off on every layer and nothing in the bake that was meant to touch them.
//
// transfer_facet_paint() rather than ModelVolume::restore_painting(): the latter's remap wants
// a target triangle overlapping the source paint to within 0.01 mm, which the baked surface is
// nowhere near - it has moved by the relief depth. Nothing to do when the topology is intact:
// triangle i is still triangle i, so the paint is already exactly right and carrying it at all
// could only lose detail.
std::array<TriangleSelector::TriangleSplittingData, 4> before;
if (topology_changed) {
before[0] = volume->supported_facets.get_data();
before[1] = volume->seam_facets.get_data();
before[2] = volume->mmu_segmentation_facets.get_data();
before[3] = volume->fuzzy_skin_facets.get_data();
}
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
if (topology_changed) {
const std::array<FacetsAnnotation *, 4> target{ &volume->supported_facets, &volume->seam_facets,
&volume->mmu_segmentation_facets,
&volume->fuzzy_skin_facets };
for (size_t ch = 0; ch < target.size(); ++ch) {
const std::vector<uint8_t> state =
transfer_facet_paint(m_input.base_mesh, before[ch], volume->mesh().its);
// Nothing was painted in this channel, or there is nothing to paint it onto: either
// way the annotation must not keep describing the mesh that is gone.
if (state.empty()) {
target[ch]->reset();
continue;
}
TriangleSelector selector(volume->mesh());
bool any = false;
for (size_t i = 0; i < state.size(); ++i)
if (state[i] != 0) {
selector.set_facet(int(i), EnforcerBlockerType(state[i]));
any = true;
}
if (any)
target[ch]->set(selector);
else
target[ch]->reset();
}
}
// 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.
// colour for everything outside it. What was already painted is whatever the transfer above
// left in the annotation, so this bake's colour lands on top of it rather than instead of it.
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();
@@ -159,7 +217,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old
// topology is exactly what makes the gizmo reload an empty selector over a non-empty mask and
// then erase it on the next flush - see GLGizmoTextureDisplacement::update_from_model_object().
const bool topology_changed = m_input.base_mesh.indices.size() != volume->mesh().its.indices.size();
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot) {
const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
@@ -167,6 +224,14 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
volume->texture_displacement_facet(slot).reset();
}
// The model's own UV channel. Resampled onto a rebuilt mesh from the surface the bake started
// from: a displaced vertex came from its foot point on that surface, which is the position its
// uv belongs to - so no distance limit, the relief depth is exactly how far it has moved.
if (!m_input.model_uv.empty())
volume->texture_uv = topology_changed ? transfer_uv_channel(m_input.base_mesh, m_input.model_uv,
volume->mesh().its) :
m_input.model_uv;
ModelObject *object = volume->get_object();
if (object == nullptr)
return;
@@ -219,6 +284,8 @@ void queue_texture_displacement_bake(const ModelVolume &volume, const TextureCol
input.take_snapshot = take_snapshot;
input.volume_id = volume.id();
input.base_mesh = volume.mesh().its;
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
input.layers = volume.texture_displacement_layers;
input.options = volume.texture_displacement_options;
input.volume_to_world = texture_displacement_volume_to_world(volume);
@@ -34,6 +34,9 @@ struct TextureDisplacementBakeInput
// writes the volume's mmu_segmentation_facets - the same per-triangle filament assignment the MMU
// paint gizmo writes - alongside the displaced geometry.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), empty when it has none: what a ModelUV
// layer maps through, and what the commit puts back on the baked mesh.
std::vector<Vec2f> model_uv;
// 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.
@@ -17,6 +17,7 @@
#include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory>
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TriangleMesh.hpp"
namespace Slic3r::GUI {
@@ -52,13 +53,14 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
indexed_triangle_set mesh = m_input.base_mesh;
TextureDisplacementFacetsData masks = m_input.facets_data;
std::vector<Vec2f> uv = m_input.model_uv;
// The default pipeline can only move vertices the mesh already has, so its recipe starts with the
// preparation - which is where "first it remeshes, then it refines" actually happens. The
// experimental pipeline refines as part of the bake and has nothing to prepare.
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
const TextureDisplacementPrepareResult prepared =
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, uv, m_input.prepare_params,
m_input.color,
// Preparation is roughly half the run; the bake
// takes the progress bar from there.
@@ -77,12 +79,14 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
if (!prepared.mesh.indices.empty()) {
mesh = prepared.mesh;
masks = prepared.masks;
uv = prepared.uv; // resampled onto the prepared mesh, as the prepare commit stores it
}
}
build_texture_displacement(mesh, m_input.layers, masks, m_input.options,
[&report](int pct) { return report(50 + pct / 2); },
/* color */ nullptr, m_input.volume_to_world, &recorder);
/* color */ nullptr, m_input.volume_to_world, &recorder, /* stats */ nullptr,
uv.empty() ? nullptr : &uv);
m_stages = recorder.take();
ctl.update_status(100, status); // always finish at 100: this is what closes the notification
@@ -115,6 +119,8 @@ void queue_texture_displacement_debug(const ModelVolume &volume, const TextureCo
input.check_topology = check_topology;
input.color = color;
input.volume_to_world = texture_displacement_volume_to_world(volume);
if (volume.has_texture_uv())
input.model_uv = volume.texture_uv;
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = volume.texture_displacement_facet(i).get_data();
@@ -47,6 +47,9 @@ struct TextureDisplacementDebugInput
// Only the quantizer is used, and only by the refinement's colour criterion - the debug run
// produces no colour of its own.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so the recipe captured here is the one a
// ModelUV layer actually bakes through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
// The edge scan behind the open / non-manifold counts is a sort over every half-edge, which on a
// multi-million triangle stage costs more than the stage did. On by default because a stage that
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
// 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,
m_input.model_uv, m_input.params, m_input.color,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
@@ -106,6 +106,10 @@ void TextureDisplacementPrepareJob::finalize(bool canceled, std::exception_ptr &
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)]));
// The model's own UV channel, resampled onto the new mesh by the run. Also put back after
// restore_painting(), which resets it along with the extra facets for the same reason: it is
// indexed by triangle and the triangles have changed.
volume->texture_uv = std::move(m_result.uv);
if (ObjectList *obj_list = wxGetApp().obj_list()) {
const ModelObjectPtrs &objs = plater->model().objects;
@@ -9,6 +9,7 @@
#include "Job.hpp"
#include "libslic3r/ObjectID.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
namespace Slic3r {
@@ -47,6 +48,10 @@ struct TextureDisplacementPrepareInput
// 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 volume's own UV channel (ModelVolume::texture_uv), empty when it has none. prepare_mesh()
// resamples it onto the mesh it produces, and the commit puts it back - like the masks, it is
// indexed by triangle and so cannot survive the stages on its own.
std::vector<Vec2f> model_uv;
// 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;
@@ -48,7 +48,8 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
(!m_current_generation ||
m_current_generation->load() == m_generation);
},
color, m_input.volume_to_world);
color, m_input.volume_to_world, nullptr, nullptr,
m_input.model_uv.empty() ? nullptr : &m_input.model_uv);
}
void TextureDisplacementPreviewJob::finalize(bool canceled, std::exception_ptr &eptr)
@@ -29,6 +29,9 @@ struct TextureDisplacementPreviewInput
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
TextureColorSettings color;
// The volume's own UV channel (ModelVolume::texture_uv), so a ModelUV layer previews through the
// same layout it will bake through. Empty when the volume has none.
std::vector<Vec2f> model_uv;
};
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
-6
View File
@@ -1,6 +0,0 @@
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
-11
View File
@@ -1,11 +0,0 @@
#ifndef slic3r_MultiTaskModel_hpp_
#define slic3r_MultiTaskModel_hpp_
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
#endif
-232
View File
@@ -1,232 +0,0 @@
#include "SysInfoDialog.hpp"
#include "I18N.hpp"
#include "3DScene.hpp"
#include "GUI.hpp"
#include "../Utils/UndoRedo.hpp"
#include "Plater.hpp"
#include <string>
#include <Eigen/Core>
#include <wx/clipbrd.h>
#include <wx/platinfo.h>
#include "GUI_App.hpp"
#include "MainFrame.hpp"
#include "wxExtensions.hpp"
#include "libslic3r/BlacklistedLibraryCheck.hpp"
#include "libslic3r/Color.hpp"
#include "format.hpp"
#ifdef _WIN32
// The standard Windows includes.
#define WIN32_LEAN_AND_MEAN
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#include <psapi.h>
#endif /* _WIN32 */
namespace Slic3r {
namespace GUI {
std::string get_main_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
if (!format_as_html)
out << b_start << (wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME) << b_end << line_end;
out << b_start << "Version: " << b_end << SLIC3R_VERSION << line_end;
out << b_start << "Build: " << b_end << (wxGetApp().is_editor() ? SLIC3R_BUILD_ID : GCODEVIEWER_BUILD_ID) << line_end;
out << line_end;
out << b_start << "Operating System: " << b_end << wxPlatformInfo::Get().GetOperatingSystemFamilyName() << line_end;
out << b_start << "System Architecture: " << b_end << wxPlatformInfo::Get().GetArchName() << line_end;
out << b_start <<
#if defined _WIN32
"Windows Version: "
#else
// Hopefully some kind of unix / linux.
"System Version: "
#endif
<< b_end << wxPlatformInfo::Get().GetOperatingSystemDescription() << line_end;
out << b_start << "Total RAM size [MB]: " << b_end << Slic3r::format_memsize_MB(Slic3r::total_physical_memory());
return out.str();
}
std::string get_mem_info(bool format_as_html)
{
std::stringstream out;
std::string b_start = format_as_html ? "<b>" : "";
std::string b_end = format_as_html ? "</b>" : "";
std::string line_end = format_as_html ? "<br>" : "\n";
std::string mem_info_str = log_memory_info(true);
std::istringstream mem_info(mem_info_str);
std::string value;
while (std::getline(mem_info, value, ':')) {
out << b_start << (value+": ") << b_end;
std::getline(mem_info, value, ';');
out << value << line_end;
}
const Slic3r::UndoRedo::Stack &stack = wxGetApp().plater()->undo_redo_stack_main();
out << b_start << "RAM size reserved for the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.get_memory_limit()) << line_end;
out << b_start << "RAM size occupied by the Undo / Redo stack: " << b_end << Slic3r::format_memsize_MB(stack.memsize()) << line_end << line_end;
return out.str();
}
SysInfoDialog::SysInfoDialog()
: DPIDialog(static_cast<wxWindow*>(wxGetApp().mainframe), wxID_ANY, (wxGetApp().is_editor() ? wxString(SLIC3R_APP_FULL_NAME) : wxString(GCODEVIEWER_APP_NAME)) + " - " + _L("System Information"), wxDefaultPosition, wxDefaultSize, wxDEFAULT_DIALOG_STYLE | wxRESIZE_BORDER)
{
wxColour bgr_clr = wxGetApp().get_window_default_clr();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOW);
SetBackgroundColour(bgr_clr);
SetFont(wxGetApp().normal_font());
wxBoxSizer* hsizer = new wxBoxSizer(wxHORIZONTAL);
hsizer->SetMinSize(wxSize(50 * wxGetApp().em_unit(), -1));
auto main_sizer = new wxBoxSizer(wxVERTICAL);
main_sizer->Add(hsizer, 1, wxEXPAND | wxALL, 10);
// logo
m_logo_bmp = ScalableBitmap(this, wxGetApp().logo_name(), 192);
m_logo = new wxStaticBitmap(this, wxID_ANY, m_logo_bmp.bmp());
hsizer->Add(m_logo, 0, wxALIGN_CENTER_VERTICAL);
wxBoxSizer* vsizer = new wxBoxSizer(wxVERTICAL);
hsizer->Add(vsizer, 1, wxEXPAND|wxLEFT, 20);
// title
{
wxStaticText* title = new wxStaticText(this, wxID_ANY, wxGetApp().is_editor() ? SLIC3R_APP_FULL_NAME : GCODEVIEWER_APP_NAME, wxDefaultPosition, wxDefaultSize);
wxFont title_font = wxGetApp().bold_font();
title_font.SetFamily(wxFONTFAMILY_ROMAN);
title_font.SetPointSize(22);
title->SetFont(title_font);
vsizer->Add(title, 0, wxEXPAND | wxTOP, wxGetApp().em_unit()/*50*/);
}
// main_info_text
wxFont font = get_default_font(this);
const auto text_clr = wxGetApp().get_label_clr_default();//wxSystemSettings::GetColour(wxSYS_COLOUR_WINDOWTEXT);
auto text_clr_str = encode_color(ColorRGB(text_clr.Red(), text_clr.Green(), text_clr.Blue()));
auto bgr_clr_str = encode_color(ColorRGB(bgr_clr.Red(), bgr_clr.Green(), bgr_clr.Blue()));
const int fs = font.GetPointSize() - 1;
int size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_NEVER);
{
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_html->SetBorders(2);
const auto text = wxString::Format(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
get_main_info(true));
m_html->SetPage(text);
vsizer->Add(m_html, 1, wxEXPAND | wxBOTTOM, wxGetApp().em_unit());
}
// opengl_info
m_opengl_info_html = new wxHtmlWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHW_SCROLLBAR_AUTO);
{
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * wxGetApp().em_unit()));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), size);
m_opengl_info_html->SetBorders(10);
wxString blacklisted_libraries_message;
#ifdef WIN32
std::wstring blacklisted_libraries = BlacklistedLibraryCheck::get_instance().get_blacklisted_string().c_str();
if (! blacklisted_libraries.empty())
blacklisted_libraries_message = wxString("<br><b>") + _L("Blacklisted libraries loaded into OrcaSlicer process:") + "</b><br>" + blacklisted_libraries;
#endif // WIN32
const auto text = GUI::format_wxstr(
"<html>"
"<body bgcolor= %s link= %s>"
"<font color=%s>"
"%s<br>%s<br>%s<br>%s"
"</font>"
"</body>"
"</html>", bgr_clr_str, text_clr_str, text_clr_str,
blacklisted_libraries_message,
get_mem_info(true), wxGetApp().get_gl_info(false),
"<b>" + _L("SIMD is supported:") + "</b> " + Eigen::SimdInstructionSetsInUse());
m_opengl_info_html->SetPage(text);
main_sizer->Add(m_opengl_info_html, 1, wxEXPAND | wxBOTTOM, 15);
}
wxStdDialogButtonSizer* buttons = this->CreateStdDialogButtonSizer(wxOK);
m_btn_copy_to_clipboard = new wxButton(this, wxID_ANY, _L("Copy to Clipboard"), wxDefaultPosition, wxDefaultSize);
buttons->Insert(0, m_btn_copy_to_clipboard, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, 5);
m_btn_copy_to_clipboard->Bind(wxEVT_BUTTON, &SysInfoDialog::onCopyToClipboard, this);
this->SetEscapeId(wxID_OK);
this->Bind(wxEVT_BUTTON, &SysInfoDialog::onCloseDialog, this, wxID_OK);
main_sizer->Add(buttons, 0, wxEXPAND | wxRIGHT | wxBOTTOM, 3);
wxGetApp().UpdateDlgDarkUI(this, true);
// this->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
// logo->Bind(wxEVT_LEFT_DOWN, &SysInfoDialog::onCloseDialog, this);
SetSizer(main_sizer);
main_sizer->SetSizeHints(this);
}
void SysInfoDialog::on_dpi_changed(const wxRect &suggested_rect)
{
m_logo_bmp.msw_rescale();
m_logo->SetBitmap(m_logo_bmp.bmp());
wxFont font = get_default_font(this);
const int fs = font.GetPointSize() - 1;
int font_size[] = { static_cast<int>(fs*1.5), static_cast<int>(fs*1.4), static_cast<int>(fs*1.3), fs, fs, fs, fs };
m_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_html->Refresh();
const int& em = em_unit();
msw_buttons_rescale(this, em, { wxID_OK, m_btn_copy_to_clipboard->GetId() });
m_opengl_info_html->SetMinSize(wxSize(-1, 16 * em));
m_opengl_info_html->SetFonts(font.GetFaceName(), font.GetFaceName(), font_size);
m_opengl_info_html->Refresh();
const wxSize& size = wxSize(65 * em, 55 * em);
SetMinSize(size);
Fit();
Refresh();
}
void SysInfoDialog::onCopyToClipboard(wxEvent &)
{
wxTheClipboard->Open();
const auto text = get_main_info(false) + "\n" + wxGetApp().get_gl_info(true);
wxTheClipboard->SetData(new wxTextDataObject(text));
wxTheClipboard->Close();
}
void SysInfoDialog::onCloseDialog(wxEvent &)
{
this->EndModal(wxID_CLOSE);
}
} // namespace GUI
} // namespace Slic3r
-36
View File
@@ -1,36 +0,0 @@
#ifndef slic3r_GUI_SysInfoDialog_hpp_
#define slic3r_GUI_SysInfoDialog_hpp_
#include <wx/event.h>
#include <wx/wx.h>
#include <wx/html/htmlwin.h>
#include "GUI_Utils.hpp"
#include "wxExtensions.hpp"
namespace Slic3r {
namespace GUI {
class SysInfoDialog : public DPIDialog
{
ScalableBitmap m_logo_bmp;
wxStaticBitmap* m_logo;
wxHtmlWindow* m_opengl_info_html;
wxHtmlWindow* m_html;
wxButton* m_btn_copy_to_clipboard;
public:
SysInfoDialog();
protected:
void on_dpi_changed(const wxRect &suggested_rect) override;
private:
void onCopyToClipboard(wxEvent &);
void onCloseDialog(wxEvent &);
};
} // namespace GUI
} // namespace Slic3r
#endif
View File
View File
+29 -9
View File
@@ -82,6 +82,11 @@ ComboBox::ComboBox(wxWindow *parent,
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
drop.Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
drop.Bind(EVT_DISMISS, [this](auto &) {
drop_down = false;
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
@@ -251,7 +256,7 @@ int ComboBox::Append(const wxString &text,
int style)
{
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label};
item.style = style;
items.push_back(item);
SetClientDataType(wxClientData_Void);
@@ -297,6 +302,12 @@ void ComboBox::ForceDropdownOpen()
}
}
void ComboBox::CloseDropdown()
{
if (drop.IsShown())
drop.DismissAndNotify();
}
unsigned int ComboBox::GetCount() const { return items.size(); }
void ComboBox::set_replace_text(wxString text, wxString image_name)
@@ -350,6 +361,13 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
drop.Invalidate();
}
void ComboBox::SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap)
{
if (n >= items.size()) return;
items[n].action_icon = bitmap.IsOk() ? bitmap : wxNullBitmap;
drop.Invalidate();
}
int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
unsigned int pos,
void ** clientData,
@@ -357,7 +375,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
{
if (pos > this->items.size()) return -1;
for (int i = 0; i < items.GetCount(); ++i) {
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
this->items.insert(this->items.begin() + pos, item);
++pos;
}
@@ -419,16 +437,18 @@ void ComboBox::keyDown(wxKeyEvent& event)
case WXK_UP:
case WXK_DOWN:
case WXK_LEFT:
case WXK_RIGHT:
if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
SetSelection(GetSelection() - 1);
} else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
SetSelection(GetSelection() + 1);
} else {
case WXK_RIGHT: {
// Step over disabled rows: the mouse cannot select them either.
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1;
int n = GetSelection() + step;
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED))
n += step;
if (n < 0 || n >= int(items.size()))
break;
}
SetSelection(n);
sendComboBoxEvent();
break;
}
case WXK_TAB:
HandleAsNavigationKey(event);
break;
+2
View File
@@ -96,10 +96,12 @@ public:
wxBitmap GetItemBitmap(unsigned int n);
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
bool is_drop_down(){return drop_down;}
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
void ForceDropdownOpen();
void CloseDropdown();
protected:
virtual int DoInsertItems(const wxArrayStringsAdapter &items,
+64 -4
View File
@@ -30,6 +30,7 @@
#include "slic3r/GUI/wxExtensions.hpp"
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
BEGIN_EVENT_TABLE(DropDown, PopupWindow)
@@ -242,6 +243,19 @@ static wxSize GetBmpSize(wxBitmap & bmp)
#endif
}
// The x-range [first, second) of the column at the right of the rows that holds the items' action
// icons, empty when no item has one. render() draws the icons in it and mouseReleased() hit-tests
// clicks against it, so the drawn icon and its clickable area always match.
std::pair<int, int> DropDown::actionColumn() const
{
if (actionIconWidth <= 0)
return {0, 0};
int right = rowSize.x;
if (rowSize.y * count > GetSize().y) // position bar
right -= 6;
return {right - actionIconWidth - FromDIP(16), right};
}
static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
{
// save dc
@@ -371,6 +385,10 @@ void DropDown::render(wxDC &dc)
rcContent.width -= szBmp.x + 5;
}
const auto action_column = actionColumn();
if (action_column.second > action_column.first)
rcContent.width = action_column.first - rcContent.x;
std::set<wxString> groups;
// draw texts & icons
int index = 0;
@@ -434,13 +452,24 @@ void DropDown::render(wxDC &dc)
auto text = group.IsEmpty()
? (item.group_key.IsEmpty() ? item.text : item.group_label)
: item.text;
if (item.action_icon.IsOk()) {
auto action_size = GetBmpSize(item.action_icon);
wxPoint action_pt(action_column.second - action_size.x - FromDIP(8),
rcContent.y + (rcContent.height - action_size.y) / 2);
dc.DrawBitmap(item.action_icon, action_pt);
}
if (!text_off && !text.IsEmpty()) {
wxSize tSize = dc.GetMultiLineTextExtent(text);
if (pt.x + tSize.x > rcContent.GetRight()) {
if (is_hover && item.tip.IsEmpty())
SetToolTip(text);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
rcContent.GetRight() - pt.x);
int max_text_width = rcContent.GetRight() - pt.x;
if (max_text_width <= 0) {
text.clear();
} else {
if (is_hover && item.tip.IsEmpty())
SetToolTip(text);
text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END, max_text_width);
}
}
pt.y += (rcContent.height - textSize.y) / 2;
dc.SetFont(GetFont());
@@ -529,6 +558,7 @@ void DropDown::messureSize()
if (!need_sync) return;
textSize = wxSize();
iconSize = wxSize();
actionIconWidth = 0;
count = 0;
wxClientDC dc(GetParent() ? GetParent() : this);
dc.SetFont(GetFont());
@@ -565,8 +595,13 @@ void DropDown::messureSize()
size1.x += size2.x + (text_off ? 0 : 5);
}
}
if (item.action_icon.IsOk())
actionIconWidth = std::max(actionIconWidth, GetBmpSize(item.action_icon).x);
if (size1.x > textSize.x) textSize = size1;
}
// Every row leaves room for the action column, as render() and actionColumn() lay it out.
if (actionIconWidth > 0)
textSize.x += actionIconWidth + FromDIP(16);
if (!align_icon) iconSize.x = 0;
wxSize szContent = textSize;
if (szContent.x < FromDIP(120) && !use_content_width)
@@ -636,6 +671,11 @@ void DropDown::messureSize()
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
}
need_sync = false;
}
@@ -714,6 +754,15 @@ void DropDown::mouseReleased(wxMouseEvent& event)
if (hover_item < 0)
return;
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved
int index = hoverIndex();
if (index >= 0 && items[index].action_icon.IsOk()) {
const auto action_column = actionColumn();
if (event.GetX() >= action_column.first && event.GetX() < action_column.second) {
sendDropDownActionEvent(index);
paintNow();
return;
}
}
sendDropDownEvent();
if (mainDropDown)
mainDropDown->hover_item = -1; // To Dismiss mainDropDown
@@ -830,6 +879,17 @@ void DropDown::sendDropDownEvent()
GetEventHandler()->ProcessEvent(event);
}
void DropDown::sendDropDownActionEvent(int index)
{
if (index < 0 || index >= int(items.size()) || (items[index].style & DD_ITEM_STYLE_DISABLED))
return;
wxCommandEvent event(EVT_DROPDOWN_ITEM_ACTION, GetId());
event.SetEventObject(this);
event.SetInt(index);
event.SetString(items[index].text);
GetEventHandler()->ProcessEvent(event);
}
void DropDown::Dismiss()
{
if (subDropDown && subDropDown->IsShown())
+6
View File
@@ -5,6 +5,7 @@
#include <wx/event.h>
#include <vector>
#include <cstddef>
#include <utility>
#include <wx/gdicmn.h>
#include "slic3r/GUI/Widgets/StateColor.hpp"
#include <boost/date_time/posix_time/ptime.hpp>
@@ -27,6 +28,7 @@ class wxWindow;
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
class DropDown : public PopupWindow
{
@@ -37,6 +39,7 @@ public:
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
wxBitmap icon;
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
wxBitmap action_icon;
void * data{nullptr};
wxString group_key{};
wxString group_label{};
@@ -65,6 +68,7 @@ private:
wxSize textSize;
wxSize iconSize;
int actionIconWidth = 0;
wxSize rowSize;
StateHandler state_handler;
@@ -135,6 +139,7 @@ private:
int hoverIndex();
int selectedItem();
std::pair<int, int> actionColumn() const;
friend class ComboBox;
void messureSize();
@@ -148,6 +153,7 @@ private:
void mouseWheelMoved(wxMouseEvent &event);
void sendDropDownEvent();
void sendDropDownActionEvent(int index);
DECLARE_EVENT_TABLE()
-15
View File
@@ -1,15 +0,0 @@
#ifndef slic3r_wxinit_hpp_
#define slic3r_wxinit_hpp_
#include <wx/wx.h>
#include <wx/intl.h>
#include <wx/html/htmlwin.h>
// Perl redefines a _ macro, so we undef this one
#undef _
// We do want to use translation however, so define it as __ so we can do a find/replace
// later when we no longer need to undef _
#define __(s) wxGetTranslation((s))
#endif
-19
View File
@@ -1,19 +0,0 @@
#ifndef slic3r_GUI_Profile_hpp_
#define slic3r_GUI_Profile_hpp_
// Profiling support using the Shiny intrusive profiler
//#define SLIC3R_PROFILE_GUI
#if defined(SLIC3R_PROFILE) && defined(SLIC3R_PROFILE_GUI)
#include <Shiny/Shiny.h>
#define SLIC3R_GUI_PROFILE_FUNC() PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name) PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE() PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x) PROFILE_OUTPUT(x)
#else
#define SLIC3R_GUI_PROFILE_FUNC()
#define SLIC3R_GUI_PROFILE_BLOCK(name)
#define SLIC3R_GUI_PROFILE_UPDATE()
#define SLIC3R_GUI_PROFILE_OUTPUT(x)
#endif
#endif // slic3r_GUI_Profile_hpp_
+178 -13
View File
@@ -28,6 +28,7 @@
#include <miniz.h>
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include <memory>
#include <cstdint>
#include <ios>
@@ -38,6 +39,7 @@
#include <boost/algorithm/string/predicate.hpp>
#include <algorithm>
#include <functional>
#include <initializer_list>
#include <catch2/catch_tostring.hpp>
#include <Eigen/Core>
@@ -349,6 +351,24 @@ static bool replace_in_3mf_entry(const std::string& path, const std::string& suf
return replaced;
}
// Stores `model` as a one-plate project.
static void store_project(Model& model, const std::string& path, SaveStrategy strategy)
{
ScopedTemporaryDir backup_dir("orca_paint_src");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig cfg;
PlateData plate;
plate.plate_index = 0;
StoreParams sp;
sp.path = path.c_str();
sp.model = &model;
sp.config = &cfg;
sp.strategy = strategy;
sp.plate_data_list.push_back(&plate);
REQUIRE(store_bbs_3mf(sp));
}
// Stores a one-plate project holding a cube whose first two facets are painted Extruder2 and
// Extruder3, which the exporter writes as paint_color="8" and paint_color="0C".
static void store_painted_cube(const std::string& path)
@@ -363,19 +383,7 @@ static void store_painted_cube(const std::string& path)
selector.set_facet(1, EnforcerBlockerType::Extruder3);
REQUIRE(volume->mmu_segmentation_facets.set(selector));
}
ScopedTemporaryDir backup_dir("orca_paint_src");
model.set_backup_path(backup_dir.string());
DynamicPrintConfig cfg;
PlateData plate;
plate.plate_index = 0;
StoreParams sp;
sp.path = path.c_str();
sp.model = &model;
sp.config = &cfg;
sp.strategy = SaveStrategy::Zip64 | SaveStrategy::Silence;
sp.plate_data_list.push_back(&plate);
REQUIRE(store_bbs_3mf(sp));
store_project(model, path, SaveStrategy::Zip64 | SaveStrategy::Silence);
}
// Loads `path` through the BBS importer into `model`, releasing the plates it returns. The
@@ -501,6 +509,163 @@ TEST_CASE("A project with malformed paint data loads without the damaged facet",
REQUIRE(selector.num_facets(EnforcerBlockerType::Extruder3) == 1);
}
// Paints `triangles` of `volume` on texture displacement layer slot `slot`.
static void paint_texture_slot(ModelVolume& volume, int slot, std::initializer_list<int> triangles)
{
TriangleSelector selector(volume.mesh());
for (const int triangle : triangles)
selector.set_facet(triangle, EnforcerBlockerType::ENFORCER);
REQUIRE(volume.texture_displacement_facet(slot).set(selector));
}
// Every triangle's texture paint on every slot, in the form the exporter writes it, so two volumes
// compare equal only when the same triangles are painted on the same slots.
static std::vector<std::string> texture_paint(const ModelVolume& volume)
{
std::vector<std::string> paint;
for (int slot = 0; slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++slot)
for (size_t i = 0; i < volume.mesh().its.indices.size(); ++i)
paint.push_back(volume.texture_displacement_facet(slot).get_triangle_as_string(int(i)));
return paint;
}
// A cube with one texture displacement layer in slot 2 and texture paint on gapped triangles of two
// slots. The layer's image bytes need not decode: the archive stores them as they are.
static ModelVolume& add_texture_displaced_cube(Model& model)
{
ModelObject* object = model.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
TextureDisplacementLayer layer;
layer.slot = 2;
layer.name = "Bark";
layer.depth_mm = 1.25f;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::vector<unsigned char>{ 0x89, 'P', 'N', 'G', 1, 2, 3 });
volume->texture_displacement_layers.push_back(layer);
volume->texture_displacement_options.active_slot = 2;
paint_texture_slot(*volume, 2, { 3, 7 });
paint_texture_slot(*volume, 5, { 10 });
return *volume;
}
TEST_CASE("Texture displacement layers, textures and paint survive a project round trip", "[3mf]")
{
// A single model file, and one file per object (read by a separate importer).
const SaveStrategy strategy = GENERATE(SaveStrategy::Zip64 | SaveStrategy::Silence,
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel);
INFO("save strategy " << int(strategy));
ScopedTemporaryFile temp(".3mf");
Model source;
const ModelVolume& painted = add_texture_displaced_cube(source);
const std::vector<std::string> paint = texture_paint(painted);
REQUIRE(std::count_if(paint.begin(), paint.end(), [](const std::string& s) { return !s.empty(); }) == 3);
store_project(source, temp.string(), strategy);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 1);
const ModelVolume& volume = *model.objects.front()->volumes.front();
REQUIRE(volume.texture_displacement_layers.size() == 1);
const TextureDisplacementLayer& layer = volume.texture_displacement_layers.front();
CHECK(layer.slot == 2);
CHECK(layer.name == "Bark");
CHECK_THAT(layer.depth_mm, Catch::Matchers::WithinAbs(1.25f, 1e-6f));
REQUIRE(layer.image_data);
CHECK(*layer.image_data == *painted.texture_displacement_layers.front().image_data);
CHECK(volume.texture_displacement_options.active_slot == 2);
CHECK(texture_paint(volume) == paint);
}
TEST_CASE("Volumes sharing a mesh keep their own texture displacement paint", "[3mf]")
{
Model source;
ModelObject* first_object = source.add_object();
ModelVolume* first = first_object->add_volume(make_cube(10., 10., 10.));
first_object->add_instance();
ModelVolume* second = source.add_object(*first_object)->volumes.front();
REQUIRE(second->mesh_ptr() == first->mesh_ptr());
paint_texture_slot(*first, 0, { 3 });
paint_texture_slot(*second, 0, { 7 });
// What a project save from the app uses: one file per object, an identical mesh written once.
ScopedTemporaryFile temp(".3mf");
store_project(source, temp.string(),
SaveStrategy::Zip64 | SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::ShareMesh);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 2);
CHECK(texture_paint(*model.objects[0]->volumes.front()) == texture_paint(*first));
CHECK(texture_paint(*model.objects[1]->volumes.front()) == texture_paint(*second));
}
TEST_CASE("A texture displacement layer never loads its image from outside the project", "[3mf]")
{
// A file the layer names, on the machine opening the project, that the project did not carry.
ScopedTemporaryFile texture(".png");
{
boost::nowide::ofstream file(texture.string(), std::ios::binary);
file << "not part of the project";
}
REQUIRE(boost::filesystem::file_size(texture.path()) > 0);
Model source;
ModelObject* object = source.add_object();
ModelVolume* volume = object->add_volume(make_cube(10., 10., 10.));
object->add_instance();
TextureDisplacementLayer layer;
layer.slot = 0;
layer.name = "Elsewhere";
layer.path = texture.string();
layer.depth_mm = 0.75f;
volume->texture_displacement_layers.push_back(layer);
ScopedTemporaryFile temp(".3mf");
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
REQUIRE(load_project(temp.string(), model, backup_dir));
REQUIRE(model.objects.size() == 1);
const std::vector<TextureDisplacementLayer>& layers = model.objects.front()->volumes.front()->texture_displacement_layers;
REQUIRE(layers.size() == 1);
CHECK(layers.front().name == "Elsewhere");
CHECK_THAT(layers.front().depth_mm, Catch::Matchers::WithinAbs(0.75f, 1e-6f));
CHECK_FALSE(layers.front().image_data);
}
TEST_CASE("A project with a malformed texture displacement value still loads", "[3mf][Regression]")
{
const auto [from, to] = GENERATE(table<std::string, std::string>({
{ "\"islands\":[]", "\"islands\":[[null,0,0,1]]" }, // a number that is null
{ "\"slot\":2", "\"slot\":8" }, // a slot past the last
}));
INFO(from << " -> " << to);
ScopedTemporaryFile temp(".3mf");
Model source;
const ModelVolume& painted = add_texture_displaced_cube(source);
store_project(source, temp.string(), SaveStrategy::Zip64 | SaveStrategy::Silence);
REQUIRE(replace_in_3mf_entry(temp.string(), ".json", from, to));
ScopedTemporaryDir backup_dir("orca_texdisp_dst");
Model model;
bool loaded = false;
REQUIRE_NOTHROW(loaded = load_project(temp.string(), model, backup_dir));
REQUIRE(loaded);
REQUIRE(model.objects.size() == 1);
const ModelVolume& volume = *model.objects.front()->volumes.front();
// The layer stack is dropped as a whole; the paint, stored apart from it, still loads.
CHECK(volume.texture_displacement_layers.empty());
CHECK(texture_paint(volume) == texture_paint(painted));
}
// The recipe lives only in the BBS-native backend, because that is the only one that runs:
// store_bbs_3mf is the sole exporter the app calls, and 3mf.cpp's load_3mf is reached only for
// files fingerprinted as PrusaSlicer's, which never carry a recipe. This locks in both halves:
@@ -32,6 +32,7 @@
#include <fstream>
#include <limits>
#include <set>
#include <string>
#include <boost/filesystem.hpp>
#include "libslic3r/AABBMesh.hpp"
@@ -2157,6 +2158,177 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
CHECK(second <= single * 5 / 4);
}
// The .3mf stores the layer stack as JSON (see texture_displacement_layers_to_json). Two properties
// matter: everything the UV editor and the panel can set has to survive a round trip, and a document
// written by a build that knew fewer fields has to keep loading, with the missing ones left at their
// defaults rather than zeroed.
TEST_CASE("Texture displacement layers survive a JSON round trip", "[TextureDisplacement]")
{
std::vector<TextureDisplacementLayer> layers(2);
TextureDisplacementLayer &a = layers[0];
a.slot = 0;
a.name = "Bark";
a.path = "/textures/bark.png";
a.depth_mm = 1.25f;
a.tiling_scale = 7.5f;
a.rotation_deg = 30.f;
a.offset = Vec2f(0.25f, -0.5f);
a.invert = true;
a.midlevel = 0.125f;
a.smoothing = 0.75f;
a.edge_smoothing = true;
a.edge_smoothing_amount = 0.25f;
a.auto_connect_islands = false;
a.tile_enabled = false;
a.tile_method = TextureTileMethod::MirroredRepeat;
a.projection_method = TextureProjectionMethod::LSCM;
a.blend_mode = TextureBlendMode::Subtract;
a.color_enabled = true;
a.lscm_seam_angle_deg = 45.f;
a.island_padding_mm = 0.5f;
a.lscm_seam_edges = { { 1, 2 }, { 3, 5 } };
a.islands = { TextureIsland{ Vec2f(1.f, 2.f), 15.f, 1.5f }, TextureIsland{} };
a.island_groups = { 0, 0 };
a.lscm_uv_overrides = { { -4, Vec2f(0.5f, 0.75f) } };
TextureDisplacementLayer &b = layers[1];
b.slot = 1;
b.projection_method = TextureProjectionMethod::ViewProjected;
b.view_project_right = Vec3f(0.f, 1.f, 0.f);
b.view_project_up = Vec3f(0.f, 0.f, 1.f);
b.view_project_projective = true;
for (size_t i = 0; i < b.view_project_matrix.size(); ++i)
b.view_project_matrix[i] = float(i) + 0.5f;
TextureDisplacementOptions options;
options.displace_border = false;
options.smooth_enabled = true;
options.smooth_strength = 0.6f;
options.smooth_iterations = 5;
options.pipeline_v2 = false;
options.v2_max_triangles_k = 250;
options.color_mix_count = 3;
options.color_despeckle = 4;
options.active_slot = 3;
options.panel_mode = 1;
const std::string json = texture_displacement_layers_to_json(layers, options);
std::vector<TextureDisplacementLayer> read_layers;
TextureDisplacementOptions read_options;
REQUIRE(texture_displacement_layers_from_json(json, read_layers, read_options));
REQUIRE(read_layers.size() == layers.size());
const TextureDisplacementLayer &ra = read_layers[0];
CHECK(ra.name == a.name);
CHECK(ra.path == a.path);
CHECK_THAT(ra.depth_mm, WithinAbs(a.depth_mm, 1e-6f));
CHECK_THAT(ra.tiling_scale, WithinAbs(a.tiling_scale, 1e-6f));
CHECK_THAT(ra.rotation_deg, WithinAbs(a.rotation_deg, 1e-6f));
CHECK(ra.offset.isApprox(a.offset));
CHECK(ra.invert == a.invert);
CHECK_THAT(ra.midlevel, WithinAbs(a.midlevel, 1e-6f));
CHECK_THAT(ra.smoothing, WithinAbs(a.smoothing, 1e-6f));
CHECK(ra.edge_smoothing == a.edge_smoothing);
CHECK_THAT(ra.edge_smoothing_amount, WithinAbs(a.edge_smoothing_amount, 1e-6f));
CHECK(ra.auto_connect_islands == a.auto_connect_islands);
CHECK(ra.tile_enabled == a.tile_enabled);
CHECK(ra.tile_method == a.tile_method);
CHECK(ra.projection_method == a.projection_method);
CHECK(ra.blend_mode == a.blend_mode);
CHECK(ra.color_enabled == a.color_enabled);
CHECK_THAT(ra.lscm_seam_angle_deg, WithinAbs(a.lscm_seam_angle_deg, 1e-6f));
CHECK_THAT(ra.island_padding_mm, WithinAbs(a.island_padding_mm, 1e-6f));
CHECK(ra.lscm_seam_edges == a.lscm_seam_edges);
REQUIRE(ra.islands.size() == a.islands.size());
CHECK(ra.islands[0].offset.isApprox(a.islands[0].offset));
CHECK_THAT(ra.islands[0].rotation_deg, WithinAbs(a.islands[0].rotation_deg, 1e-6f));
CHECK_THAT(ra.islands[0].scale, WithinAbs(a.islands[0].scale, 1e-6f));
CHECK(ra.island_groups == a.island_groups);
REQUIRE(ra.lscm_uv_overrides.size() == 1);
CHECK(ra.lscm_uv_overrides[0].first == -4);
CHECK(ra.lscm_uv_overrides[0].second.isApprox(Vec2f(0.5f, 0.75f)));
const TextureDisplacementLayer &rb = read_layers[1];
CHECK(rb.projection_method == TextureProjectionMethod::ViewProjected);
CHECK(rb.view_project_right.isApprox(b.view_project_right));
CHECK(rb.view_project_up.isApprox(b.view_project_up));
CHECK(rb.view_project_projective);
CHECK(rb.view_project_matrix == b.view_project_matrix);
CHECK(read_options.displace_border == options.displace_border);
CHECK(read_options.smooth_enabled == options.smooth_enabled);
CHECK_THAT(read_options.smooth_strength, WithinAbs(options.smooth_strength, 1e-6f));
CHECK(read_options.smooth_iterations == options.smooth_iterations);
CHECK(read_options.pipeline_v2 == options.pipeline_v2);
CHECK(read_options.v2_max_triangles_k == options.v2_max_triangles_k);
CHECK(read_options.color_mix_count == options.color_mix_count);
CHECK(read_options.color_despeckle == options.color_despeckle);
// Where the panel was left comes back with the rest, so a reopened project is on the same layer.
CHECK(read_options.active_slot == options.active_slot);
CHECK(read_options.panel_mode == options.panel_mode);
}
TEST_CASE("Texture displacement JSON keeps defaults for keys it does not carry", "[TextureDisplacement]")
{
// What a build that knew fewer fields would have written: one layer, almost nothing set.
const std::string sparse = R"({"version":1,"layers":[{"slot":2,"name":"Old","depth_mm":0.75}]})";
std::vector<TextureDisplacementLayer> layers;
TextureDisplacementOptions options;
REQUIRE(texture_displacement_layers_from_json(sparse, layers, options));
REQUIRE(layers.size() == 1);
const TextureDisplacementLayer &l = layers[0];
const TextureDisplacementLayer fresh;
const TextureDisplacementOptions defaults;
CHECK(l.slot == 2);
CHECK(l.name == "Old");
CHECK_THAT(l.depth_mm, WithinAbs(0.75f, 1e-6f));
// Everything absent keeps the struct's own default rather than becoming zero.
CHECK_THAT(l.tiling_scale, WithinAbs(fresh.tiling_scale, 1e-6f));
CHECK(l.auto_connect_islands == fresh.auto_connect_islands);
CHECK(l.tile_enabled == fresh.tile_enabled);
CHECK(l.projection_method == fresh.projection_method);
CHECK_THAT(l.lscm_seam_angle_deg, WithinAbs(fresh.lscm_seam_angle_deg, 1e-6f));
CHECK(options.color_mix_count == defaults.color_mix_count);
CHECK(options.pipeline_v2 == defaults.pipeline_v2);
CHECK(options.active_slot == defaults.active_slot);
CHECK(options.panel_mode == defaults.panel_mode);
std::vector<TextureDisplacementLayer> unused_layers;
TextureDisplacementOptions unused_options;
CHECK(!texture_displacement_layers_from_json("not json at all", unused_layers, unused_options));
}
TEST_CASE("Texture displacement JSON with a wrong-typed element or an out-of-range slot is rejected", "[TextureDisplacement]")
{
const std::string layer = GENERATE(as<std::string>{},
R"({"slot":0,"offset":["x",0]})",
R"({"slot":0,"view_project_right":[null,0,0]})",
R"({"slot":0,"view_project_matrix":[0,0,0,0,0,0,0,0,0,0,0,"x"]})",
R"({"slot":0,"lscm_seam_edges":[1,null]})",
R"({"slot":0,"islands":[[0,0,"x",1]]})",
R"({"slot":0,"lscm_uv_overrides":[[null,0,0]]})",
R"({"slot":8})",
R"({"slot":-1})",
R"({"slot":"x"})",
R"({"name":"No slot"})");
INFO(layer);
std::vector<TextureDisplacementLayer> layers(1);
layers.front().name = "Kept";
TextureDisplacementOptions options;
options.color_mix_count = 7;
bool parsed = true;
REQUIRE_NOTHROW(parsed = texture_displacement_layers_from_json(R"({"version":1,"layers":[)" + layer + "]}", layers, options));
CHECK_FALSE(parsed);
REQUIRE(layers.size() == 1);
CHECK(layers.front().name == "Kept");
CHECK(options.color_mix_count == 7);
}
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
{
TextureDisplacementLayer gray;
@@ -2174,3 +2346,353 @@ TEST_CASE("whether a layer's texture has colour agrees with its decode", "[Textu
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
}
// ---------------------------------------------------------------------------------------------
// The model's own UV channel, and the Override blend mode
// ---------------------------------------------------------------------------------------------
// A grayscale image that is black in its left half and white in its right half, so where a sample
// lands along u is readable straight off the height it comes back with.
static std::shared_ptr<std::vector<unsigned char>> make_left_right_split_png(size_t w = 16, size_t h = 16)
{
std::vector<uint8_t> pixels(w * h);
for (size_t y = 0; y < h; ++y)
for (size_t x = 0; x < w; ++x)
pixels[y * w + x] = (x < w / 2) ? 0 : 255;
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
REQUIRE(Slic3r::png::write_gray_to_file(tmp_path.string(), w, h, pixels));
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
REQUIRE_FALSE(bytes.empty());
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// Where a sheet coordinate lands in uv: across [UV_LO, UV_LO + UV_SPAN] rather than the full [0, 1].
// The inset keeps the tests off the exact edge of an untiled placement, where there is deliberately no
// texture at all rather than its border texel (see DecodedHeightTexture::sample()) - a real model's
// channel does reach 1.0, and a vertex sitting exactly there displaces by nothing on purpose.
static constexpr float UV_LO = 0.1f;
static constexpr float UV_SPAN = 0.8f;
static float sheet_uv_at(float coord, float size, bool mirror = false)
{
const float t = coord / size;
return UV_LO + UV_SPAN * (mirror ? 1.f - t : t);
}
// One UV per triangle corner: u follows x, v follows y. `mirror_u` mirrors u, which is what the tests
// use to show the bake is really reading the channel rather than projecting the position.
static std::vector<Vec2f> sheet_uv_channel(const indexed_triangle_set &sheet, float size, bool mirror_u = false)
{
std::vector<Vec2f> uv(sheet.indices.size() * 3);
for (size_t f = 0; f < sheet.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = sheet.vertices[size_t(sheet.indices[f][k])];
uv[f * 3 + size_t(k)] = Vec2f(sheet_uv_at(p.x(), size, mirror_u), sheet_uv_at(p.y(), size));
}
return uv;
}
TEST_CASE("Override blend replaces the displacement under it instead of combining with it",
"[TextureDisplacement]")
{
CHECK_THAT(blend_displacement(3.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
// Nothing underneath yet is still just the value, so an Override layer at the bottom of the
// stack behaves exactly like the Add the bake forces there.
CHECK_THAT(blend_displacement(0.f, 1.f, TextureBlendMode::Override), WithinAbs(1.f, 1e-6f));
// The sign carries, and a zero sample really does flatten what is under it - that is the point.
CHECK_THAT(blend_displacement(3.f, -1.f, TextureBlendMode::Override), WithinAbs(-1.f, 1e-6f));
CHECK_THAT(blend_displacement(3.f, 0.f, TextureBlendMode::Override), WithinAbs(0.f, 1e-6f));
}
TEST_CASE("an Override layer leaves only its own relief where two layers overlap", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
TextureDisplacementLayer base;
base.slot = 0;
base.depth_mm = 2.0f;
base.tiling_scale = 5.0f;
base.image_data = make_flat_gray_png(255); // full depth everywhere
TextureDisplacementLayer second = base;
second.slot = 1;
second.depth_mm = 0.5f;
second.blend_mode = TextureBlendMode::Override;
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(cube);
facets[1] = facets[0];
const indexed_triangle_set overridden = build_texture_displacement(cube, {base, second}, facets, classic_options());
REQUIRE(overridden.vertices.size() == cube.vertices.size());
// Add would have piled the two up to 2.5 mm; Override keeps the upper layer's 0.5 mm alone.
TextureDisplacementLayer added = second;
added.blend_mode = TextureBlendMode::Add;
const indexed_triangle_set summed = build_texture_displacement(cube, {base, added}, facets, classic_options());
REQUIRE(summed.vertices.size() == cube.vertices.size());
for (size_t i = 0; i < cube.vertices.size(); ++i) {
if (!above_resting_plane(cube, i))
continue;
CHECK_THAT((overridden.vertices[i] - cube.vertices[i]).norm(), WithinAbs(0.5f, 1e-3f));
CHECK_THAT((summed.vertices[i] - cube.vertices[i]).norm(), WithinAbs(2.5f, 1e-3f));
}
}
TEST_CASE("the ModelUV mapping displaces through the model's own UV channel", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 1.f);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.image_data = make_left_right_split_png();
layer.tile_enabled = false; // the layout is the placement; nothing should repeat
layer.projection_method = TextureProjectionMethod::ModelUV;
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(sheet);
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
const std::vector<Vec2f> flipped = sheet_uv_channel(sheet, size, /* mirror_u */ true);
const indexed_triangle_set out = build_texture_displacement(sheet, {layer}, facets, classic_options(), {}, nullptr,
Transform3d::Identity(), nullptr, nullptr, &uv);
REQUIRE(out.vertices.size() == sheet.vertices.size());
// The image is black for u < 0.5 and white above it, and the channel's u rises with x across the
// sheet, so it comes out flat on its left half and raised by the full depth on its right.
size_t low = 0, high = 0;
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
const float x = sheet.vertices[i].x();
const float dz = out.vertices[i].z() - sheet.vertices[i].z();
if (x < 0.4f * size) {
CHECK_THAT(dz, WithinAbs(0.f, 1e-3f));
++low;
} else if (x > 0.6f * size) {
CHECK_THAT(dz, WithinAbs(layer.depth_mm, 1e-3f));
++high;
}
}
REQUIRE(low > 0);
REQUIRE(high > 0);
// Mirroring the channel mirrors the relief: the mapping really is the channel's, not a planar
// projection of the position (which would be unchanged by it).
const indexed_triangle_set mirrored = build_texture_displacement(sheet, {layer}, facets, classic_options(), {},
nullptr, Transform3d::Identity(), nullptr, nullptr,
&flipped);
REQUIRE(mirrored.vertices.size() == sheet.vertices.size());
for (size_t i = 0; i < sheet.vertices.size(); ++i) {
const float x = sheet.vertices[i].x();
if (x < 0.4f * size)
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(layer.depth_mm, 1e-3f));
else if (x > 0.6f * size)
CHECK_THAT(mirrored.vertices[i].z() - sheet.vertices[i].z(), WithinAbs(0.f, 1e-3f));
}
}
TEST_CASE("a ModelUV layer falls back to Triplanar when the volume carries no UV channel",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 2.f);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.tiling_scale = 5.0f;
layer.image_data = make_flat_gray_png(255);
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(sheet);
layer.projection_method = TextureProjectionMethod::Triplanar;
const indexed_triangle_set as_triplanar = build_texture_displacement(sheet, {layer}, facets, classic_options());
layer.projection_method = TextureProjectionMethod::ModelUV;
const indexed_triangle_set no_channel = build_texture_displacement(sheet, {layer}, facets, classic_options());
REQUIRE(no_channel.vertices.size() == as_triplanar.vertices.size());
for (size_t i = 0; i < as_triplanar.vertices.size(); ++i)
CHECK_THAT((no_channel.vertices[i] - as_triplanar.vertices[i]).norm(), WithinAbs(0.f, 1e-5f));
}
TEST_CASE("a bake with colour off on every layer reports no colour at all", "[TextureDisplacement]")
{
const indexed_triangle_set cube = its_make_cube(10., 10., 10.);
TextureDisplacementLayer layer;
layer.slot = 0;
layer.depth_mm = 1.0f;
layer.tiling_scale = 5.0f;
layer.image_data = make_flat_rgb_png(200, 40, 10); // has colour to give...
layer.color_enabled = false; // ...but is not asked for it
TextureDisplacementFacetsData facets{};
facets[0] = paint_whole_mesh(cube);
std::vector<uint8_t> triangle_color;
TextureColorRequest request;
request.quantize = [](const Vec3f &) { return 0; }; // would colour everything, if asked
request.out_triangle = &triangle_color;
const indexed_triangle_set out = build_texture_displacement(cube, {layer}, facets, classic_options(), {}, &request);
REQUIRE_FALSE(out.vertices.empty());
// Every entry 0 ("this triangle takes no colour from the texture"), so the commit leaves whatever
// the model was already painted with exactly as it was.
for (const uint8_t entry : triangle_color)
CHECK(entry == 0);
}
TEST_CASE("transfer_uv_channel resamples a UV channel onto a subdivided copy of the same surface",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 5.f);
const indexed_triangle_set fine = make_flat_sheet(size, 1.f);
const std::vector<Vec2f> uv = sheet_uv_channel(coarse, size);
const std::vector<Vec2f> moved = transfer_uv_channel(coarse, uv, fine);
REQUIRE(moved.size() == fine.indices.size() * 3);
// The channel is affine in position here (u = x/size, v = y/size), so a correct transfer
// reproduces it exactly at every corner of the finer mesh.
for (size_t f = 0; f < fine.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = fine.vertices[size_t(fine.indices[f][k])];
CHECK_THAT(moved[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
CHECK_THAT(moved[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
}
// A mismatched channel is refused rather than read past its end.
CHECK(transfer_uv_channel(coarse, {Vec2f::Zero()}, fine).empty());
CHECK(transfer_uv_channel(coarse, uv, indexed_triangle_set{}).empty());
}
TEST_CASE("the model's per-corner UVs survive a brush split and collapse to one per vertex",
"[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set sheet = make_flat_sheet(size, 5.f);
const std::vector<Vec2f> uv = sheet_uv_channel(sheet, size);
// The whole sheet as a patch, with the source map the channel is read through.
const TriangleMesh tm(sheet);
TriangleSelector selector(tm);
for (int f = 0; f < int(sheet.indices.size()); ++f)
selector.set_facet(f, EnforcerBlockerType::ENFORCER);
std::vector<int> source;
const indexed_triangle_set patch = selector.get_facets_strict(EnforcerBlockerType::ENFORCER, &source);
REQUIRE(patch.indices.size() == source.size());
const std::vector<Vec2f> corner = compute_model_corner_uvs(sheet, uv, patch, source);
REQUIRE(corner.size() == patch.indices.size() * 3);
// An unsplit patch triangle *is* its source triangle, so its corners reproduce the channel.
for (size_t f = 0; f < patch.indices.size(); ++f)
for (int k = 0; k < 3; ++k) {
const Vec3f &p = patch.vertices[size_t(patch.indices[f][k])];
CHECK_THAT(corner[f * 3 + size_t(k)].x(), WithinAbs(sheet_uv_at(p.x(), size), 1e-4f));
CHECK_THAT(corner[f * 3 + size_t(k)].y(), WithinAbs(sheet_uv_at(p.y(), size), 1e-4f));
}
const std::vector<Vec2f> per_vertex = collapse_corner_uvs(patch, corner);
REQUIRE(per_vertex.size() == patch.vertices.size());
for (size_t i = 0; i < patch.vertices.size(); ++i) {
CHECK_THAT(per_vertex[i].x(), WithinAbs(sheet_uv_at(patch.vertices[i].x(), size), 1e-4f));
CHECK_THAT(per_vertex[i].y(), WithinAbs(sheet_uv_at(patch.vertices[i].y(), size), 1e-4f));
}
// Mismatched inputs report nothing rather than guessing.
CHECK(compute_model_corner_uvs(sheet, {Vec2f::Zero()}, patch, source).empty());
CHECK(compute_model_corner_uvs(sheet, uv, patch, {}).empty());
CHECK(collapse_corner_uvs(patch, {Vec2f::Zero()}).empty());
}
TEST_CASE("facet paint is carried onto a rebuilt mesh by each facet's foot point", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
// Two filaments, split down the middle - the pattern the transfer has to reproduce.
const TriangleMesh coarse_mesh(coarse);
TriangleSelector painter(coarse_mesh);
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
painter.set_facet(int(t), x < 0.5f * size ? EnforcerBlockerType::Extruder1 : EnforcerBlockerType::Extruder2);
}
const TriangleSelector::TriangleSplittingData paint = painter.serialize();
// A finer mesh, lifted clear of the original surface the way a bake's relief lifts it. This is
// what ModelVolume::restore_painting()'s 0.01 mm overlap remap cannot follow.
indexed_triangle_set lifted = make_flat_sheet(size, 1.f);
for (Vec3f &v : lifted.vertices)
v.z() += 0.5f;
const std::vector<uint8_t> state = transfer_facet_paint(coarse, paint, lifted);
REQUIRE(state.size() == lifted.indices.size());
size_t left = 0, right = 0;
for (size_t t = 0; t < lifted.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = lifted.indices[t];
const float x = (lifted.vertices[size_t(tri[0])].x() + lifted.vertices[size_t(tri[1])].x() +
lifted.vertices[size_t(tri[2])].x()) / 3.f;
// Only away from the split: a facet straddling it may legitimately land on either side.
if (x < 0.4f * size) {
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder1));
++left;
} else if (x > 0.6f * size) {
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder2));
++right;
}
}
REQUIRE(left > 0);
REQUIRE(right > 0);
// An unpainted source has nothing to carry, which is not the same as carrying NONE everywhere:
// the caller has to tell those apart to decide between resetting the annotation and writing one.
CHECK(transfer_facet_paint(coarse, TriangleSelector::TriangleSplittingData{}, lifted).empty());
CHECK(transfer_facet_paint(coarse, paint, indexed_triangle_set{}).empty());
}
TEST_CASE("an unpainted part of the source carries no paint onto the rebuilt mesh", "[TextureDisplacement]")
{
constexpr float size = 10.f;
const indexed_triangle_set coarse = make_flat_sheet(size, 2.5f);
// Only the left half is painted at all; the right half must come back NONE rather than borrowing
// the nearest painted colour from across the model.
const TriangleMesh coarse_mesh(coarse);
TriangleSelector painter(coarse_mesh);
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
if (x < 0.5f * size)
painter.set_facet(int(t), EnforcerBlockerType::Extruder3);
}
const std::vector<uint8_t> state = transfer_facet_paint(coarse, painter.serialize(), coarse);
REQUIRE(state.size() == coarse.indices.size());
for (size_t t = 0; t < coarse.indices.size(); ++t) {
const stl_triangle_vertex_indices &tri = coarse.indices[t];
const float x = (coarse.vertices[size_t(tri[0])].x() + coarse.vertices[size_t(tri[1])].x() +
coarse.vertices[size_t(tri[2])].x()) / 3.f;
if (x < 0.4f * size)
CHECK(state[t] == uint8_t(EnforcerBlockerType::Extruder3));
else if (x > 0.6f * size)
CHECK(state[t] == uint8_t(EnforcerBlockerType::NONE));
}
}