Compare commits

..
Author SHA1 Message Date
Lam Wei Lun 2b736dd268 Flatpak: drop the WebKit NVIDIA export; the binary owns the workaround
/proc/driver/nvidia/version is readable inside the Flatpak sandbox, so the
binary's NVIDIA branch already sets WEBKIT_DMABUF_RENDERER_FORCE_SHM with
the correct runtime gate and the entrypoint's own export is redundant.

Verified with:
  flatpak run --command=cat com.orcaslicer.OrcaSlicer /proc/driver/nvidia/version
which prints the NVRM version (driver 615.71.09) from inside the sandbox.
2026-10-12 15:49:58 +08:00
Lam Wei Lun 5dba611eac Flatpak: force WebKit's shared-memory transport instead of disabling DMABUF
The entrypoint exported WEBKIT_DISABLE_DMABUF_RENDERER=1 for NVIDIA. WebKit
reads that variable first and leaves the transport set empty, which turns
accelerated compositing off, so the Device tab still lagged for NVIDIA
Flatpak users, and the binary could not override it because its setenv is
non-replacing.

The runtime's WebKitGTK is vanilla, so there is no distro isNVIDIA() guard
to clear and WEBKIT_DMABUF_RENDERER_FORCE_SHM=1 alone is the right switch:
compositing stays on and only the hardware DMABUF path is skipped.
2026-10-12 15:06:30 +08:00
Lam Wei Lun 2feb5f0f9d Linux: drop the AppImage WebKit DMABUF exports; correct the skill notes
The launcher cannot gate on the runtime WebKitGTK version, so exporting
WEBKIT_DMABUF_RENDERER_FORCE_SHM/WEBKIT_FORCE_DMABUF_RENDERER there would
clear a distro isNVIDIA() guard on hosts older than 2.44, where FORCE_SHM
is ignored, re-exposing the broken hardware path. The binary already sets
both with the correct gate, so remove them and say who owns the workaround.

Also correct two claims in the skill reference. An empty transport set only
means "no accelerated compositing" from 2.44 on; 2.42/2.43 still fall back
to the Wayland/X11 renderers, removed in the same release that added
FORCE_SHM. And threaded scrolling is off by default in 2.44/2.45 (on from
2.46), so FORCE_SHM keeps compositing rather than scrolling. Document the
module-level detection cost and the WEBKIT_DMABUF_RENDERER_FORCE_SHM=0
opt-out.
2026-10-12 13:07:02 +08:00
Lam Wei Lun 9c164ca597 Linux: use WebKit's shared-memory transport for the NVIDIA DMABUF renderer
The Device tab web view is slow on NVIDIA proprietary drivers even with
accelerated compositing left on: since the DMABUF renderer became the
default in WebKitGTK 2.42, the driver cannot back it, and web views fall
back to a slow path. Gating the old WEBKIT_DISABLE_COMPOSITING_MODE
fallback on WebKit < 2.42 is the wrong axis, because the failure is
version-independent from 2.42 on.

Force the shared-memory transport with WEBKIT_DMABUF_RENDERER_FORCE_SHM=1.
That keeps compositing and threaded scrolling and skips only the hardware
path. Also set WEBKIT_FORCE_DMABUF_RENDERER=1, which clears the isNVIDIA()
early return that Debian/Ubuntu/openSUSE patch into
rendererBufferTransportMode() and which otherwise returns before the
FORCE_SHM check is reached.

Both are set before the X11-vs-Wayland split so the workaround also covers
XWayland, and only when WebKitGTK is new enough to honour FORCE_SHM
(>= 2.44, WebView::SupportsForceSharedMemory()).

The AppImage launcher exported the superseded
WEBKIT_DISABLE_DMABUF_RENDERER=1, which is read first, leaves the transport
set empty and so disables compositing outright; replace it with the same
pair. The wxWidgets skill reference is updated to match.
2026-10-12 13:02:10 +08:00
Lam Wei Lun 00bccf9b2d Linux: only disable WebKit compositing for WebKitGTK older than 2.42
The default Wayland path set WEBKIT_DISABLE_COMPOSITING_MODE=1 whenever the
session had both DISPLAY and WAYLAND_DISPLAY, i.e. any Wayland session with
XWayland. That forces WebKitGTK accelerated compositing off and also disables
threaded scrolling, so the Device tab web view (Mainsail/Fluidd) lagged unless
the app was launched with GDK_BACKEND=x11, the only path that skipped the
variable. Disabling compositing also turns off threaded scrolling.

The XWayland compositing bug the fallback worked around is fixed in WebKitGTK
>= 2.42, so apply it only for older WebKit. WebView::NeedsCompositingWorkaround()
reads the runtime WebKitGTK version; the X11 opt-in path is unchanged.

Behavior is unchanged on WebKit < 2.42, on pure X11, and on native Wayland
without XWayland. The wxWidgets skill reference is updated to match.
2026-10-12 11:46:31 +08:00
117 changed files with 3409 additions and 6633 deletions
@@ -569,12 +569,16 @@ on the **first** call. So:
#endif #endif
``` ```
**Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) branches on **Before GTK starts**, only the environment exists. `CLI::run` (`src/OrcaSlicer.cpp`) sets the NVIDIA
`GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On the default path it forces `GDK_BACKEND=x11` DMABUF workaround and then branches on `GDK_BACKEND` (an `x11` prefix means the X11 opt-in). On both
when wx lacks EGL and `WAYLAND_DISPLAY` is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` paths, when `/proc/driver/nvidia/version` exists and the linked WebKitGTK honours the switch (>= 2.44,
(non-replacing) only when both `DISPLAY` and `WAYLAND_DISPLAY` are set, and calls `XInitThreads()` `WebView::SupportsForceSharedMemory()`), it sets `WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and
`WEBKIT_FORCE_DMABUF_RENDERER=1` (non-replacing): the proprietary driver cannot back WebKit's hardware
DMABUF renderer. On the default path it forces `GDK_BACKEND=x11` when wx lacks EGL and `WAYLAND_DISPLAY`
is set. It sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` (non-replacing) only when both `DISPLAY` and
`WAYLAND_DISPLAY` are set and the linked WebKitGTK is older than 2.42, and calls `XInitThreads()`
only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor only when `DISPLAY` is set. With neither variable set, the GUI refuses to start ("Neither DISPLAY nor
WAYLAND_DISPLAY set"). The details and the WebKit rule (c12912e0df) are in WAYLAND_DISPLAY set"). The details and the WebKit rules (c12912e0df) are in
`references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions. `references/webview-gl-aui-media.md` §WebKitGTK on Linux sessions.
**wx's own detectors**, for reference: **wx's own detectors**, for reference:
@@ -61,7 +61,8 @@ available; GTK2/WebKit1 is an opt-out configuration the GUI does not support.
11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that 11. A web host that re-themes in place handles `EVT_WEBVIEW_RECREATED` without `Skip()`; one that
needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview) needs a reload lets it through. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview) 12. Keep `WebViewWebKit`'s destructor removing the `"wx"` handler. §[Orca wrapper](#orca-webview-wrapper-widgetswebview)
13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case, and never 13. Do not widen `WEBKIT_DISABLE_COMPOSITING_MODE` beyond Orca's default-path XWayland case for
WebKitGTK older than 2.42, and never
make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions) make a page's progress depend only on a C++→JS callback. §[WebKitGTK on Linux](#webkitgtk-on-linux-sessions)
14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and 14. GL attribute lists: legacy `int[]` lists end with `0` and spell out `WX_GL_RGBA` and
`WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested `WX_GL_DOUBLEBUFFER`; `wxGLAttributes`/`wxGLContextAttrs` end with `EndList()`; MSAA is requested
@@ -567,33 +568,66 @@ vue-resize workaround script and sets a persistent SQLite cookie store on the vi
## WebKitGTK on Linux sessions ## WebKitGTK on Linux sessions
**Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises) **Mechanism.** `CLI::run` (`src/OrcaSlicer.cpp`, under `#ifdef __WXGTK__`, before GTK initialises)
branches on `GDK_BACKEND`: sets the NVIDIA DMABUF workaround and then branches on `GDK_BACKEND`:
- **Both paths**, before the split: when `/proc/driver/nvidia/version` exists and the linked WebKitGTK
honours the switch (>= 2.44, `WebView::SupportsForceSharedMemory()`), it sets
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=1` and `WEBKIT_FORCE_DMABUF_RENDERER=1` non-replacing. The
proprietary driver cannot back WebKit's hardware DMABUF renderer, so the shared-memory transport keeps
accelerated compositing instead of dropping it; `WEBKIT_FORCE_DMABUF_RENDERER` clears the
Debian/Ubuntu/openSUSE `isNVIDIA()` early return, which otherwise returns before the FORCE_SHM check.
- **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and - **Default path** (`GDK_BACKEND` unset or not starting with `x11`): if wx lacks EGL (`!wxHAS_EGL`) and
`WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1` `WAYLAND_DISPLAY` is set, it forces `GDK_BACKEND=x11`; it sets `WEBKIT_DISABLE_COMPOSITING_MODE=1`
non-replacing **only** when both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with non-replacing **only** when the linked WebKitGTK predates 2.42 (`WebView::NeedsCompositingWorkaround()`)
**and** both `DISPLAY` and `WAYLAND_DISPLAY` are non-empty (a Wayland session with
XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when XWayland available — the session, not the backend GTK will pick); `XInitThreads()` runs only when
`DISPLAY` is set. `DISPLAY` is set.
- **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when - **`GDK_BACKEND=x11…` opt-in path**: sets `DRI_PRIME=1` (and the NVIDIA PRIME variables when
`/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable `/proc/driver/nvidia/version` exists), calls `XInitThreads()`, and deliberately does **not** disable
compositing (the comment: the WebKit2GTK XWayland bug appears fixed in WebKit2GTK ≥ 2.42, and leaving compositing, which keeps hardware acceleration for Device / Setup Wizard / login pages.
compositing on keeps hardware acceleration for Device / Setup Wizard / login pages).
Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()` Runtime backend detection elsewhere uses Orca's `is_running_on_wayland()` / `is_running_on_x11()`
(see `references/platforms.md`), not these environment variables. (see `references/platforms.md`), not these environment variables.
**Pitfall.** **Pitfall.**
- **Rule:** On NVIDIA reach for `WEBKIT_DMABUF_RENDERER_FORCE_SHM` before
`WEBKIT_DISABLE_COMPOSITING_MODE`. **Why:** the proprietary driver cannot back WebKit's hardware
DMABUF renderer, leaving web views blank or slow; FORCE_SHM keeps accelerated compositing and only
skips the hardware path, while disabling compositing drops it. Threaded scrolling is a separate,
release-dependent default (off in 2.44/2.45, on from 2.46). `WEBKIT_DISABLE_DMABUF_RENDERER` is not a
substitute — it is read first, leaves the transport set empty (no compositing from 2.44 on, where the
X11/Wayland fallback renderers were removed) and has a reported crash path. On Debian/Ubuntu and
openSUSE the `isNVIDIA()` patch returns before the FORCE_SHM check, so `WEBKIT_FORCE_DMABUF_RENDERER=1`
must clear it first; that variable exists only in those patches. WebKit reads all of these once per
process, before the first WebView.
The test is `/proc/driver/nvidia/version`, so a hybrid laptop that renders on its iGPU also takes the
shared-memory path, which is a per-frame GPU readback rather than zero-copy; exporting
`WEBKIT_DMABUF_RENDERER_FORCE_SHM=0` in the environment restores the hardware path.
```cpp
// Wrong: kills compositing and threaded scrolling (and can crash on modern WebKit)
::setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1", false);
// Right: keep compositing, skip only the hardware DMABUF path
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /*replace*/ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /*replace*/ false);
}
```
Cite: WebKit 2.52.6 `Source/WebKit/UIProcess/gtk/AcceleratedBackingStore.cpp`
(`rendererBufferTransportMode()`), Debian `disable-nvidia-dmabuf.patch`, WebKit bug 262607.
- **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions, - **Rule:** Set `WEBKIT_DISABLE_COMPOSITING_MODE` only on the default path in Wayland sessions,
non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK non-replacing (`setenv(..., false)`, so a value the user already exported wins), before GTK
initialises; never unconditionally. Keep JS-side fallbacks so a page never initialises; never unconditionally, and only for the WebKitGTK releases that need it (older than
2.42, `WebView::NeedsCompositingWorkaround()`). Keep JS-side fallbacks so a page never
waits forever for a C++→JS call. waits forever for a C++→JS call.
**Why:** WebKit2GTK accelerated compositing could fail silently under XWayland, leaving web views **Why:** WebKit2GTK accelerated compositing could fail silently under XWayland before 2.42, leaving
blank (Setup Wizard stuck on "Loading..."); disabling it everywhere needlessly drops hardware web views blank (Setup Wizard stuck on "Loading..."); the variable also turns off threaded scrolling,
acceleration; without the JS timeout the user is stuck when the signal is lost. so applying it on a newer WebKit needlessly drops both GPU compositing and async scrolling (the
Device tab loses its smoothness); without the JS timeout the user is stuck when the signal is lost.
```cpp ```cpp
// Wrong: unconditional, kills HW acceleration everywhere // Wrong: unconditional, kills HW acceleration everywhere
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", false);
// Right (default path only): non-replacing, before GTK init, Wayland session with XWayland // Right (default path only): non-replacing, before GTK init, WebKitGTK < 2.42,
if (display && *display && wayland && *wayland) // Wayland session with XWayland
if (WebView::NeedsCompositingWorkaround() && display && *display && wayland && *wayland)
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /*replace*/ false);
``` ```
Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit` Cite: c12912e0df (`src/OrcaSlicer.cpp`; `resources/web/guide/0/load.js` `OnInit`
+6 -2
View File
@@ -341,12 +341,12 @@ jobs:
# Notarize main DMG # Notarize main DMG
xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait xcrun notarytool submit "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
retry xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg xcrun stapler staple OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Notarize profile validator DMG if it exists # Notarize profile validator DMG if it exists
if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then if [ -f "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" ]; then
xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait xcrun notarytool submit "OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg" --apple-id "${{ secrets.APPLE_DEV_ACCOUNT }}" --team-id "${{ secrets.TEAM_ID }}" --password "${{ secrets.APP_PWD }}" --wait
retry xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi fi
- name: Create DMG without notary - name: Create DMG without notary
@@ -460,6 +460,10 @@ jobs:
- name: Build slicer Win - name: Build slicer Win
if: runner.os == 'Windows' if: runner.os == 'Windows'
working-directory: ${{ github.workspace }} working-directory: ${{ github.workspace }}
# Orca: Removed Netfabb STL fixing service support in favor of CGAL.
# env:
# WindowsSdkDir: 'C:\Program Files (x86)\Windows Kits\10\'
# WindowsSDKVersion: '10.0.26100.0\'
# --tests builds the unit tests too; the unit_tests_windows_* jobs run them. # --tests builds the unit tests too; the unit_tests_windows_* jobs run them.
run: | run: |
# cache-path is the install directory inside the deps build directory. # cache-path is the install directory inside the deps build directory.
+4 -70
View File
@@ -16,15 +16,6 @@ on:
permissions: permissions:
contents: read contents: read
# Listing caches and builds while waiting for a deps cache.
actions: read
# Read by scripts/ci_deps_cache.sh.
env:
REPO: ${{ github.repository }}
WORKFLOW: build_all.yml
BASE_REF: ${{ github.base_ref }}
JOB: build_linux (${{ vars.SELF_HOSTED && 'orca-lnx-server' || 'ubuntu-24.04' }})
concurrency: concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }} group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
@@ -44,7 +35,7 @@ jobs:
# The PR merge commit plus its first parent, the base it is diffed against. # The PR merge commit plus its first parent, the base it is diffed against.
fetch-depth: 2 fetch-depth: 2
- name: Look for changed files - name: Look for changed C++ files
id: changes id: changes
run: | run: |
if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then if git -c core.quotePath=false diff --name-only HEAD^1 -- src tests | grep -qE '\.(cpp|cc|cxx|hpp|h|hxx)$'; then
@@ -52,75 +43,18 @@ jobs:
else else
echo "No C++ changes under src/ or tests/." echo "No C++ changes under src/ or tests/."
fi fi
if ! git diff --quiet HEAD^1 -- deps; then
echo "deps=true" >> "$GITHUB_OUTPUT"
fi
echo "deps-key=linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}" >> "$GITHUB_OUTPUT"
# Parsing needs the dependency headers, not a build of this PR's deps/. # Parsing needs the dependency headers, not a build of this PR's deps/, so
# Without an exact match the job waits for the build that saves it: this # a PR that changes deps/ falls back to the newest cache main has.
# PR's own when it changes deps/, otherwise a running push build of the
# base branch with the same deps. With neither, a PR that keeps deps/
# falls back to the newest cache.
- name: Restore cached deps - name: Restore cached deps
id: deps_cache
if: steps.changes.outputs.cpp == 'true' if: steps.changes.outputs.cpp == 'true'
uses: actions/cache/restore@v6 uses: actions/cache/restore@v6
with: with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }} key: linux-clang-cache-orcaslicer_deps-build-${{ hashFiles('deps/**') }}
- name: Look for a build to wait for
id: wait_check
if: steps.changes.outputs.cpp == 'true' && steps.deps_cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: bash scripts/ci_deps_cache.sh ${{ steps.changes.outputs.deps == 'true' && 'own' || 'check' }}
# Linux Build Deps takes about 35 minutes on GitHub-hosted runners. The
# wait ends early when the deps job fails.
- name: Wait for the deps cache
if: steps.wait_check.outputs.wait-for != ''
timeout-minutes: 100
env:
GH_TOKEN: ${{ github.token }}
KEY: ${{ steps.changes.outputs.deps-key }}
CACHE_REF: ${{ steps.changes.outputs.deps == 'true' && github.ref || format('refs/heads/{0}', github.base_ref) }}
KEY_INPUTS: ${{ steps.wait_check.outputs.key-inputs }}
WAIT_FOR: ${{ steps.wait_check.outputs.wait-for }}
WAIT_SINCE: ${{ steps.wait_check.outputs.wait-since }}
WAIT_MINUTES: 90
run: bash scripts/ci_deps_cache.sh wait
- name: Restore the deps cache after waiting
id: waited_cache
if: steps.wait_check.outputs.wait-for != ''
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
- name: Fail without this PR's deps cache
if: steps.changes.outputs.deps == 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
run: |
echo "::error::No deps cache ${{ steps.changes.outputs.deps-key }}. Check this PR's Linux Build Deps job, then rerun clang-tidy."
exit 1
- name: Restore the newest cached deps
id: newest_cache
if: steps.changes.outputs.deps != 'true' && steps.wait_check.outcome == 'success' && steps.waited_cache.outputs.cache-hit != 'true'
uses: actions/cache/restore@v6
with:
path: ${{ github.workspace }}/deps/build/OrcaSlicer_dep
key: ${{ steps.changes.outputs.deps-key }}
restore-keys: linux-clang-cache-orcaslicer_deps-build- restore-keys: linux-clang-cache-orcaslicer_deps-build-
fail-on-cache-miss: true fail-on-cache-miss: true
- name: Warn about older deps
if: steps.newest_cache.outcome == 'success' && steps.newest_cache.outputs.cache-hit != 'true'
run: echo "::warning title=Older deps::No cache matches this PR's deps/, so clang-tidy parsed against ${{ steps.newest_cache.outputs.cache-matched-key }}. If it reports errors in code that uses a dependency, rerun clang-tidy once the base branch's Build all has saved the new deps."
- name: Apt-Install Dependencies - name: Apt-Install Dependencies
if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED if: steps.changes.outputs.cpp == 'true' && !vars.SELF_HOSTED
uses: ./.github/actions/apt-install-deps uses: ./.github/actions/apt-install-deps
+41
View File
@@ -497,6 +497,47 @@ endif()
# Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC # Once we get rid of Perl and libslic3r is linked statically, we can get rid of -fPIC
set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# WIN10SDK_PATH is used to point CMake to the WIN10 SDK installation directory.
# We pick it from environment if it is not defined in another way
# ORCA: Removed Netfabb STL fixing service support in favor of CGAL.
# if(WIN32)
# if(NOT DEFINED WIN10SDK_PATH)
# if(DEFINED ENV{WIN10SDK_PATH})
# set(WIN10SDK_PATH "$ENV{WIN10SDK_PATH}")
# endif()
# endif()
# if(DEFINED WIN10SDK_PATH)
# #BBS: modify win10sdk_path
# if (EXISTS "${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h")
# set(WIN10SDK_INCLUDE_PATH "${WIN10SDK_PATH}")
# else()
# message("WIN10SDK_PATH is invalid: ${WIN10SDK_PATH}")
# message("${WIN10SDK_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_PATH)
# endif()
# else()
# # Try to use the default Windows 10 SDK path.
# if (DEFINED ENV{WindowsSdkDir} AND DEFINED ENV{WindowsSDKVersion})
# set(WIN10SDK_INCLUDE_PATH "$ENV{WindowsSdkDir}/Include/$ENV{WindowsSDKVersion}")
# else ()
# set(WIN10SDK_INCLUDE_PATH "C:/Program Files (x86)/Windows Kits/10/Include/10.0.26100.0")
# endif ()
# if (NOT EXISTS "${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h")
# message("${WIN10SDK_INCLUDE_PATH}/winrt/windows.graphics.printing3d.h was not found")
# message("STL fixing by the Netfabb service will not be compiled")
# unset(WIN10SDK_INCLUDE_PATH)
# endif()
# endif()
# if(WIN10SDK_INCLUDE_PATH)
# message("Building with Win10 Netfabb STL fixing service support")
# add_definitions(-DHAS_WIN10SDK)
# include_directories(SYSTEM "${WIN10SDK_INCLUDE_PATH}")
# else()
# message("Building without Win10 Netfabb STL fixing service support")
# endif()
# endif()
if (APPLE) if (APPLE)
message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}") message("OS X SDK Path: ${CMAKE_OSX_SYSROOT}")
if (CMAKE_OSX_DEPLOYMENT_TARGET) if (CMAKE_OSX_DEPLOYMENT_TARGET)
-8
View File
@@ -1,18 +1,10 @@
# ccache does not parse the -clang: arguments CMake uses for clang-cl's gcc-style # 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 # 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. # 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) foreach (_lang C CXX)
if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND if (CMAKE_${_lang}_COMPILER_ID STREQUAL "Clang" AND
CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC") CMAKE_${_lang}_COMPILER_FRONTEND_VARIANT STREQUAL "MSVC")
set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes") set(CMAKE_DEPFILE_FLAGS_${_lang} "/showIncludes")
set(CMAKE_${_lang}_DEPFILE_FORMAT msvc) set(CMAKE_${_lang}_DEPFILE_FORMAT msvc)
list(APPEND CMAKE_${_lang}_COMPILE_OPTIONS_CREATE_PCH -Xclang -fno-pch-timestamp)
endif () endif ()
endforeach () endforeach ()
-135
View File
@@ -1,135 +0,0 @@
# Mesh repair — High Level Design
## Purpose and scope
Mesh repair turns a broken triangle mesh into closed, outward oriented shells that bound a solid, so that the
mesh slices predictably and the other mesh tools (booleans, cut, painting) get valid input. It runs on demand:
the *Fix Model* command of the context menu of an object or part and of the warning icon in the object list, *Fix
All* and *Fix All Plates* in the context menu of a plate, the prompt the Cut tool shows when a cut leaves open edges,
the *Repair and import* choice of texture import, and the `--repair` option of the command line. It is built on CGAL
and is the only repair the application has, on every platform.
`repair_mesh()` in `src/libslic3r/MeshRepair.cpp` does the work on one `TriangleMesh`, and `ModelVolume::repair()`
applies it to a part: it replaces the mesh only when the repair changed it, and remaps or drops the painting of a
replaced mesh. The GUI side, `fix_model_with_cgal_gui()` in `src/slic3r/Utils/FixModelByCgal.cpp`, runs it on a
worker thread for each volume of an object, or for one volume, behind a progress dialog. `repair_mesh()` reports its
progress between stages through a callback, which cancels the repair by throwing.
## What the repaired mesh is
The repair keeps what the slicer would print from the input. The slicer fills a layer where the winding number of
the input surface is not zero (`MeshSlicingParams::SlicingMode::Regular` uses Clipper's nonzero fill rule), so:
- Shells crossing each other print as their union. The repair keeps them as separate shells, as the parts of an
object are kept, and makes each of them an outward oriented solid.
- A shell inside a solid that is oriented like it adds nothing, and the repair removes it.
- A shell inside a solid that is oriented inward is a cavity, and stays one.
- A whole mesh turned inside out prints as a solid, and the repair turns it outward.
On top of that, it closes holes and cracks, gives inconsistently oriented triangles the orientation of their
neighbours, drops degenerate, duplicate and stray flat geometry, and removes small self-intersections inside a shell,
such as slivers and folds. Where none of this applies, it reports the mesh as unmodified and the caller keeps the
original mesh, its painting and its object id.
A volume is repaired in place and is never split into several parts: splitting by connectivity would turn a cavity
into a separate solid part and fill it. Each solid of the result is a shell of its own, so *Split to parts* still
separates them. Open pieces of the input whose borders meet exactly, which are pieces of one surface rather than
solids, come out as the one shell they form.
## Pipeline
The input is treated as a triangle soup. Every stage after the first only runs on what the previous ones could
not settle, so a valid mesh passes through quickly.
### Valid parts first
Parts of the input that are connected by vertex indices and are already closed, consistently oriented 2-manifolds
skip the soup stages and go straight to the shell stages. Merging their points with coincident points of other
parts would only make non-manifold junctions out of solids that touch, for example parts of an assembly exported
together or voxels written as separate cubes.
### Soup clean-up
The rest of the soup has its coincident points merged. Triangles with repeated corners are dropped. Coincident
triangles of opposite orientation are a wall between two solids touching face to face, so they cancel in pairs;
of the remaining copies of a triangle one is kept. A connected region that would vanish entirely through
cancelling is a two-sided export instead (every face written in both orientations), and keeps one copy of each
triangle.
Open edges then get their vertices welded within a tolerance of 1e-4 of the bounding box diagonal, which closes the
cracks some exporters leave between triangles that should share corners. A vertex is never welded further than a
quarter of its shortest edge, so welding cannot collapse fine detail. A soup that stays mostly open after welding has
cracks wider than that tolerance, and is welded again with ten and then a hundred times the tolerance.
### Work units
The soup is cut into units repaired independently and in parallel: its connected parts, except that open parts whose
bounding boxes touch share a unit, so that cracks and T-junctions between separately exported patches still close.
### Building the mesh
A unit whose edges are each shared by at most two triangles, running in opposite directions, and whose vertices each
have a single fan of triangles, becomes a `Surface_mesh` directly. Any other unit is oriented first. Triangles are
flipped so that neighbours across an edge shared by two of them agree, each part keeping the orientation the
majority of its area had, which keeps inward oriented cavities inward. Edges where that is impossible or that more
than two triangles share are cut, and each fan of triangles around a vertex gets its own copy of the vertex. If that
still leaves an invalid mesh, CGAL's `orient_polygon_soup()` orients the unit instead, and the parts it flipped
against their majority are reversed back. The linear pass exists because `orient_polygon_soup()` is several times
slower on large soups that need only a few fixes.
### Closing
Compatible borders are stitched, parts that can be stitched once reversed are merged, and borders still open are
snapped onto each other within the weld tolerance, which closes T-junctions. Exactly degenerate faces are removed.
Holes are filled last. Planar border loops that face each other in one plane, one inside the other, bound a single
region, such as a missing annulus at the end of a tube or a cap with features standing on it. Such a group is
triangulated at once with a 2D constrained Delaunay triangulation that fills only the faces at odd nesting depth,
so the openings inside the region stay open. The patch is used only if it closes every loop with the orientation
of the triangles around it. Every other loop is filled on its own with `triangulate_hole()`, which uses a 2D
constrained Delaunay triangulation when the loop is planar. Otherwise it searches the best triangulation among all
triangles over the loop for loops of up to 100 edges, and among the triangles of a 3D Delaunay triangulation for
longer ones, as the full search is cubic in the length of the loop. Beyond 200 edges the full search is not even
used as a fallback, and a fan around the loop's centroid closes a loop nothing else could.
The closed unit is split into its shells. The largest stays in place and the others are copied out, as converting
the whole mesh again costs seconds on large models.
### Shells
The shells of all units, together with the valid parts set aside at the start, are handled independently and in
parallel. Shells enclosing no volume, which hole filling makes out of stray flat patches, are dropped.
Self-intersections inside a shell are removed locally with `experimental::remove_self_intersections()`, which
remeshes the region around them. It is used only for local defects: a shell whose intersections involve more than
5 % of its faces is left as it is, and a result that changes the enclosed volume by more than 0.1 % is undone. On
heavily overlapping ornamental models the remeshing otherwise cuts parts down to slivers, while on genuine defects
it changes the volume by thousandths of a percent. Overlaps left in place slice correctly. Remeshing can cut
small pieces off a shell, so a shell it changed is split again and its pieces enclosing no volume are dropped.
Shells crossing others are found through their bounding boxes and an AABB tree per shell, and are made outward
oriented solids. For every other shell the winding number just outside it is summed from the orientation of the
shells containing it, tested from a point on its largest face. The shell bounds the solid only if the winding
number is zero on exactly one of its sides; it is then oriented so that the solid is inside, or dropped otherwise.
Crossing shells are not united. Slicing unites them anyway, uniting them exactly costs tens of seconds on models
made of many overlapping pieces, and it would fuse parts that *Split to parts* should keep apart.
## Robustness
The repair avoids `experimental::autorefine_and_remove_self_intersections()` and corefinement, which can crash on
messy input, and uses only steps whose failures it can detect. A step that fails keeps its input. CGAL precondition
failures surface as `repair_mesh()` returning false with the message as the error. The output is written in float,
as every mesh in the application is.
The result does not depend on thread scheduling. `libslic3r_cgal` is built with `CGAL_LINKED_WITH_TBB`. Units and
shells are processed in parallel, but each on its own data, and their results are gathered in a fixed order;
sorts compare complete keys, and the detected self-intersections are put in order before they are used. Snapping
stays sequential, as its parallel version gives different results from run to run. Holes are filled with the full
search wherever it is affordable, because the search among Delaunay triangles can choose between equally good fills
differently from run to run, so only non-planar holes of more than 100 edges may still vary. On some inputs
`experimental::remove_self_intersections()` itself places a few vertices differently from run to run.
## Verification
`tests/libslic3r/test_mesh_repair.cpp` covers each kind of defect above on small meshes with known volumes.
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7478,7 +7478,7 @@ msgid "Update"
msgstr "更新" msgstr "更新"
msgid "Assistant(HMS)" msgid "Assistant(HMS)"
msgstr "助手(HMS)" msgstr "助理(HMS)"
#, c-format, boost-format #, c-format, boost-format
msgid "Network plug-in v%s" msgid "Network plug-in v%s"
-3
View File
@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 876 B

+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "Bambulab", "name": "Bambulab",
"url": "http://www.bambulab.com/Parameters/vendor/BBL.json", "url": "http://www.bambulab.com/Parameters/vendor/BBL.json",
"version": "02.08.00.15", "version": "02.08.00.14",
"force_update": "0", "force_update": "0",
"description": "the initial version of BBL configurations", "description": "the initial version of BBL configurations",
"machine_model_list": [ "machine_model_list": [
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "Qidi", "name": "Qidi",
"version": "02.04.00.17", "version": "02.04.00.16",
"force_update": "0", "force_update": "0",
"description": "Qidi configurations", "description": "Qidi configurations",
"machine_model_list": [ "machine_model_list": [
@@ -83,7 +83,7 @@
"extruder_clearance_height_to_lid": "120", "extruder_clearance_height_to_lid": "120",
"layer_change_gcode": ";BEFORE_LAYER_CHANGE\n;[layer_z]\nLOG_Z\nG92 E0\n", "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_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=[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", "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",
"thumbnails_format": "PNG", "thumbnails_format": "PNG",
"default_filament_profile": [ "default_filament_profile": [
"Generic PLA @Qidi Q1 Pro 0.4 nozzle" "Generic PLA @Qidi Q1 Pro 0.4 nozzle"
@@ -100,7 +100,7 @@
"default_filament_profile": [ "default_filament_profile": [
"Generic PLA @Qidi" "Generic PLA @Qidi"
], ],
"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_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_end_gcode": "M104 S0\nM140 S0\n;Retract the filament\nG92 E0\nG1 E-3 F300\nG28\nM84\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" "scan_first_layer": "0"
} }
@@ -24,7 +24,7 @@
"210x210/COLPIC", "210x210/COLPIC",
"110x110/PNG" "110x110/PNG"
], ],
"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_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_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_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" "scan_first_layer": "0"
} }
@@ -72,21 +72,17 @@ varying float weight;
varying float island_active; varying float island_active;
varying vec2 vertex_uv; varying vec2 vertex_uv;
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right // The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the // the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
// cylinder is to the viewer's right rather than mirrored. // the preview wraps the texture the same way round as the bake.
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
{ {
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
right = normalize(cross(up, arbitrary)); right = normalize(cross(up, arbitrary));
fwd = normalize(cross(up, right)); fwd = normalize(cross(right, up));
} }
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
// sign(), which returns 0 there and would collapse the coordinate onto a line.
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's // The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(), // tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only. // project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
@@ -98,7 +94,7 @@ vec2 projection_raw(vec3 p, vec3 n)
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
float x = dot(rel, right); float x = dot(rel, right);
float y = dot(rel, fwd); float y = dot(rel, fwd);
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up)); return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
} }
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
@@ -106,18 +102,10 @@ vec2 projection_raw(vec3 p, vec3 n)
if (radius < 1e-8) if (radius < 1e-8)
return vec2(0.0); return vec2(0.0);
vec3 dir = rel / radius; vec3 dir = rel / radius;
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius; return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
} }
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0) vec3 an = abs(n); // Triplanar: drop the dominant normal axis
// sits at the top of the model instead of upside down, and u is flipped on the far side of each return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
// siblings, term for term.
vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z)
return vec2(box_face_sign(n.x) * p.y, -p.z);
if (an.y >= an.x && an.y >= an.z)
return vec2(-box_face_sign(n.y) * p.x, -p.z);
return vec2(box_face_sign(n.z) * p.x, -p.y);
} }
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one // The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
@@ -136,7 +124,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
vec2 xy = vec2(dot(rel, right), dot(rel, fwd)); vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
float r = length(xy); float r = length(xy);
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
b = -up; // v is the distance *against* the axis b = up; // v is the distance along the axis
return; return;
} }
if (projection_mode == 2) { if (projection_mode == 2) {
@@ -145,20 +133,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0); vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
float c = length(dir.xy); // cos(latitude) float c = length(dir.xy); // cos(latitude)
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0); t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
b = -cross(dir, t); // v grows away from the pole, unit length b = cross(dir, t); // increasing latitude, unit length
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0); units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
return; return;
} }
vec3 an = abs(n); // the same signed basis projection_raw() uses vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z) if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
t = vec3(0.0, box_face_sign(n.x), 0.0); t = vec3(0.0, 1.0, 0.0);
b = vec3(0.0, 0.0, -1.0); b = vec3(0.0, 0.0, 1.0);
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z) } else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
t = vec3(-box_face_sign(n.y), 0.0, 0.0); t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 0.0, -1.0); b = vec3(0.0, 0.0, 1.0);
} else { // planar = (+-x, -y) } else { // planar = p.xy
t = vec3(box_face_sign(n.z), 0.0, 0.0); t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, -1.0, 0.0); b = vec3(0.0, 1.0, 0.0);
} }
} }
@@ -36,20 +36,14 @@ varying vec4 world_pos;
varying float distortion; varying float distortion;
varying vec2 vertex_uv; varying vec2 vertex_uv;
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
{ {
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
right = normalize(cross(up, arbitrary)); right = normalize(cross(up, arbitrary));
fwd = normalize(cross(up, right)); fwd = normalize(cross(right, up));
} }
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
// sign(), which returns 0 there and would collapse the coordinate onto a line.
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm. // Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
vec2 projection_raw(vec3 p, vec3 n) vec2 projection_raw(vec3 p, vec3 n)
{ {
@@ -59,7 +53,7 @@ vec2 projection_raw(vec3 p, vec3 n)
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
float x = dot(rel, right); float x = dot(rel, right);
float y = dot(rel, fwd); float y = dot(rel, fwd);
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up)); return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
} }
if (projection_mode == 2) { if (projection_mode == 2) {
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
@@ -67,16 +61,10 @@ vec2 projection_raw(vec3 p, vec3 n)
if (radius < 1e-8) if (radius < 1e-8)
return vec2(0.0); return vec2(0.0);
vec3 dir = rel / radius; vec3 dir = rel / radius;
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius; return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
} }
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
vec3 an = abs(n); vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
return vec2(box_face_sign(n.x) * p.y, -p.z);
if (an.y >= an.x && an.y >= an.z)
return vec2(-box_face_sign(n.y) * p.x, -p.z);
return vec2(box_face_sign(n.z) * p.x, -p.y);
} }
vec2 project_uv(vec3 p, vec3 n) vec2 project_uv(vec3 p, vec3 n)
@@ -138,21 +138,17 @@ in vec2 vertex_uv;
out vec4 out_color; out vec4 out_color;
// The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - right // The cylinder's own frame, built exactly as libslic3r's project_cylindrical() builds it - including
// handed, so the angle taken from it grows anticlockwise about the axis, which seen from outside the // the handedness, which comes out left-handed for an axis of +Z. Copied rather than "corrected", so
// cylinder is to the viewer's right rather than mirrored. // the preview wraps the texture the same way round as the bake.
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
{ {
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
right = normalize(cross(up, arbitrary)); right = normalize(cross(up, arbitrary));
fwd = normalize(cross(up, right)); fwd = normalize(cross(right, up));
} }
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
// sign(), which returns 0 there and would collapse the coordinate onto a line.
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
// The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's // The raw, millimetre-valued projection of `p` (a position in the texture frame), before the layer's
// tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(), // tiling/rotation/aspect/offset - a term-for-term transcription of libslic3r's project_planar(),
// project_cylindrical() and project_spherical(). `n` is read by the planar mode only. // project_cylindrical() and project_spherical(). `n` is read by the planar mode only.
@@ -164,7 +160,7 @@ vec2 projection_raw(vec3 p, vec3 n)
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
float x = dot(rel, right); float x = dot(rel, right);
float y = dot(rel, fwd); float y = dot(rel, fwd);
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up)); return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
} }
if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius if (projection_mode == 2) { // Spherical: (longitude, latitude) * radius
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
@@ -172,18 +168,10 @@ vec2 projection_raw(vec3 p, vec3 n)
if (radius < 1e-8) if (radius < 1e-8)
return vec2(0.0); return vec2(0.0);
vec3 dir = rel / radius; vec3 dir = rel / radius;
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius; return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
} }
// Triplanar: drop the dominant normal axis. v is negated so the image's first pixel row (v = 0) vec3 an = abs(n); // Triplanar: drop the dominant normal axis
// sits at the top of the model instead of upside down, and u is flipped on the far side of each return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
// plane so the two faces sharing it are not mirror images. libslic3r's box_plane_yz() and its two
// siblings, term for term.
vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z)
return vec2(box_face_sign(n.x) * p.y, -p.z);
if (an.y >= an.x && an.y >= an.z)
return vec2(-box_face_sign(n.y) * p.x, -p.z);
return vec2(box_face_sign(n.z) * p.x, -p.y);
} }
// The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one // The two surface directions projection_raw()'s u and v run along at `p`, plus how many raw units one
@@ -202,7 +190,7 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
vec2 xy = vec2(dot(rel, right), dot(rel, fwd)); vec2 xy = vec2(dot(rel, right), dot(rel, fwd));
float r = length(xy); float r = length(xy);
t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it t = (r > 1e-6) ? (fwd * xy.x - right * xy.y) / r : right; // circumferential: u runs along it
b = -up; // v is the distance *against* the axis b = up; // v is the distance along the axis
return; return;
} }
if (projection_mode == 2) { if (projection_mode == 2) {
@@ -211,20 +199,20 @@ void projection_axes(vec3 p, vec3 n, out vec3 t, out vec3 b, out vec2 units_per_
vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0); vec3 dir = (r > 1e-8) ? rel / r : vec3(0.0, 0.0, 1.0);
float c = length(dir.xy); // cos(latitude) float c = length(dir.xy); // cos(latitude)
t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0); t = (c > 1e-6) ? vec3(-dir.y, dir.x, 0.0) / c : vec3(1.0, 0.0, 0.0);
b = -cross(dir, t); // v grows away from the pole, unit length b = cross(dir, t); // increasing latitude, unit length
units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0); units_per_mm = vec2(1.0 / max(c, 1e-3), 1.0);
return; return;
} }
vec3 an = abs(n); // the same signed basis projection_raw() uses vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) { // planar = (+-y, -z) if (an.x >= an.y && an.x >= an.z) { // planar = p.yz
t = vec3(0.0, box_face_sign(n.x), 0.0); t = vec3(0.0, 1.0, 0.0);
b = vec3(0.0, 0.0, -1.0); b = vec3(0.0, 0.0, 1.0);
} else if (an.y >= an.x && an.y >= an.z) { // planar = (+-x, -z) } else if (an.y >= an.x && an.y >= an.z) { // planar = p.xz
t = vec3(-box_face_sign(n.y), 0.0, 0.0); t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, 0.0, -1.0); b = vec3(0.0, 0.0, 1.0);
} else { // planar = (+-x, -y) } else { // planar = p.xy
t = vec3(box_face_sign(n.z), 0.0, 0.0); t = vec3(1.0, 0.0, 0.0);
b = vec3(0.0, -1.0, 0.0); b = vec3(0.0, 1.0, 0.0);
} }
} }
@@ -47,20 +47,14 @@ in vec2 vertex_uv;
out vec4 out_color; out vec4 out_color;
// Right handed (up x right), like libslic3r's project_cylindrical(), so the angle taken from it grows
// anticlockwise about the axis - to the viewer's right when seen from outside the cylinder.
void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd) void cylinder_frame(out vec3 up, out vec3 right, out vec3 fwd)
{ {
up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0); up = (length(patch_axis) > 1e-8) ? normalize(patch_axis) : vec3(0.0, 0.0, 1.0);
vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0); vec3 arbitrary = (abs(up.z) < 0.9) ? vec3(0.0, 0.0, 1.0) : vec3(1.0, 0.0, 0.0);
right = normalize(cross(up, arbitrary)); right = normalize(cross(up, arbitrary));
fwd = normalize(cross(up, right)); fwd = normalize(cross(right, up));
} }
// Which side of a world plane a face is on, with zero counting as the positive side. Not GLSL's own
// sign(), which returns 0 there and would collapse the coordinate onto a line.
float box_face_sign(float n) { return n >= 0.0 ? 1.0 : -1.0; }
// Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm. // Term for term libslic3r's project_planar() / project_cylindrical() / project_spherical(), in mm.
vec2 projection_raw(vec3 p, vec3 n) vec2 projection_raw(vec3 p, vec3 n)
{ {
@@ -70,7 +64,7 @@ vec2 projection_raw(vec3 p, vec3 n)
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
float x = dot(rel, right); float x = dot(rel, right);
float y = dot(rel, fwd); float y = dot(rel, fwd);
return vec2(atan(y, x) * sqrt(x * x + y * y), -dot(rel, up)); return vec2(atan(y, x) * sqrt(x * x + y * y), dot(rel, up));
} }
if (projection_mode == 2) { if (projection_mode == 2) {
vec3 rel = p - patch_center; vec3 rel = p - patch_center;
@@ -78,16 +72,10 @@ vec2 projection_raw(vec3 p, vec3 n)
if (radius < 1e-8) if (radius < 1e-8)
return vec2(0.0); return vec2(0.0);
vec3 dir = rel / radius; vec3 dir = rel / radius;
return vec2(atan(dir.y, dir.x), -asin(clamp(dir.z, -1.0, 1.0))) * radius; return vec2(atan(dir.y, dir.x), asin(clamp(dir.z, -1.0, 1.0))) * radius;
} }
// v negated so the image's first pixel row sits at the top of the model, u flipped on the far side
// of each plane so the two faces sharing it are not mirror images - see libslic3r's box_plane_yz().
vec3 an = abs(n); vec3 an = abs(n);
if (an.x >= an.y && an.x >= an.z) return (an.x >= an.y && an.x >= an.z) ? p.yz : ((an.y >= an.x && an.y >= an.z) ? p.xz : p.xy);
return vec2(box_face_sign(n.x) * p.y, -p.z);
if (an.y >= an.x && an.y >= an.z)
return vec2(-box_face_sign(n.y) * p.x, -p.z);
return vec2(box_face_sign(n.z) * p.x, -p.y);
} }
vec2 project_uv(vec3 p, vec3 n) vec2 project_uv(vec3 p, vec3 n)
+17 -43
View File
@@ -1,27 +1,21 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Lets a pull request's macOS deps build, or its clang-tidy job, wait for a # Lets a pull request's macOS deps build wait for a push build of the base
# build that saves the deps cache it needs, from a Linux job. `check` runs after # branch that builds the same deps, from a Linux job. `check` runs after a cache
# a cache miss and names a queued or running push build of the base branch with # miss and names a queued or running base build with the same deps/ and .github/
# the same deps/ and .github/ to wait for, unless the pull request changes either. # to wait for, unless the pull request changes either. `wait` waits until that
# `own` names the pull request's own build instead, for clang-tidy when the pull # build saves KEY. actions/cache then confirms the entry can be restored.
# request changes deps/. `wait` waits until that build saves KEY under CACHE_REF,
# or its deps job fails. actions/cache then confirms the entry can be restored.
# #
# Environment: GH_TOKEN, REPO, WORKFLOW_REF (or WORKFLOW, the build workflow's # Environment: GH_TOKEN, REPO, WORKFLOW_REF and BASE_REF, plus KEY_INPUTS,
# file name) and BASE_REF, plus HEAD_SHA for `own`, and KEY_INPUTS, WAIT_FOR, # WAIT_FOR, WAIT_SINCE, WAIT_MINUTES, KEY and ARCH for `wait`.
# WAIT_SINCE, WAIT_MINUTES, KEY, CACHE_REF (default the base branch) and ARCH
# (or JOB, the matrix job's name) for `wait`.
# Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT. # Writes key-inputs, wait-for and wait-since to $GITHUB_OUTPUT.
set -uo pipefail set -uo pipefail
workflow=${WORKFLOW_REF:-} workflow=${WORKFLOW_REF%%@*}
workflow=${workflow%%@*} workflow=${workflow##*/}
workflow=${WORKFLOW:-${workflow##*/}}
# Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml. # Must match the job names in build_all.yml, build_check_cache.yml and build_deps.yml.
job=${JOB:-build_macos_arch (${ARCH:-})} check_job_name="build_macos_arch (${ARCH:-}) / Check Cache"
check_job_name="$job / Check Cache" deps_job_name="build_macos_arch (${ARCH:-}) / Build Deps / Build Deps"
deps_job_name="$job / Build Deps / Build Deps"
out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; } out() { echo "$1" >> "${GITHUB_OUTPUT:-/dev/stdout}"; }
@@ -69,27 +63,8 @@ check() {
fi fi
} }
own() {
out "key-inputs=$(local_key_inputs HEAD)"
local id
# The build can be created a moment after this job.
for _ in 1 2 3 4 5; do
id=$(gh api -X GET "repos/$REPO/actions/workflows/$workflow/runs" \
-f head_sha="$HEAD_SHA" -f event=pull_request -f per_page=1 --jq '.workflow_runs[0].id // empty' < /dev/null)
[ -n "$id" ] && break
sleep 30
done
if [ -n "$id" ]; then
echo "Waiting for the deps cache of build $id of this pull request."
out "wait-for=$id"
out "wait-since=$(date +%s)"
else
echo "No build of this pull request is building its deps."
fi
}
key_saved() { key_saved() {
gh cache list -R "$REPO" --ref "${CACHE_REF:-refs/heads/$BASE_REF}" --key "$KEY" --json key < /dev/null \ gh cache list -R "$REPO" --ref "refs/heads/$BASE_REF" --key "$KEY" --json key < /dev/null \
| jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null | jq -e --arg key "$KEY" 'any(.[]; .key == $key)' > /dev/null
} }
@@ -112,11 +87,11 @@ wait_for() {
key_saved && { echo "Found $KEY."; return; } key_saved && { echo "Found $KEY."; return; }
sleep 20 sleep 20
done done
echo "Build $id finished its deps without saving $KEY." echo "Build $id of $BASE_REF finished its deps without saving $KEY."
return ;; return ;;
"completed cancelled") ;; "completed cancelled") ;;
completed*) completed*)
echo "The deps job of build $id did not succeed." echo "The deps job of build $id of $BASE_REF did not succeed."
return ;; return ;;
# The deps job exists only once the build's cache check has succeeded. # The deps job exists only once the build's cache check has succeeded.
"") "")
@@ -125,7 +100,7 @@ wait_for() {
completed*) completed*)
check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs") check=$(jq -r --arg name "$check_job_name" 'first(.jobs[] | select(.name == $name) | .conclusion) // ""' <<< "$jobs")
if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then if [ -z "$warned" ] && { [ -z "$check" ] || [ "$check" = success ]; }; then
echo "::warning title=Deps wait::Build $id has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request." echo "::warning title=Deps wait::Build $id of $BASE_REF has no job named \"$deps_job_name\". If the build jobs were renamed, update scripts/ci_deps_cache.sh. This does not affect this pull request."
warned=1 warned=1
fi ;; fi ;;
*) *)
@@ -143,12 +118,11 @@ wait_for() {
seen="$seen $id" seen="$seen $id"
echo "Following build $id of $BASE_REF." echo "Following build $id of $BASE_REF."
done done
echo "No build saved $KEY." echo "No build of $BASE_REF saved $KEY."
} }
case "${1:-}" in case "${1:-}" in
check) check ;; check) check ;;
own) own ;;
wait) wait_for ;; wait) wait_for ;;
*) echo "usage: $0 check|own|wait" >&2; exit 2 ;; *) echo "usage: $0 check|wait" >&2; exit 2 ;;
esac esac
+2 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# Work-around https://gitlab.gnome.org/GNOME/gnome-build-meta/-/issues/754 # WebKit's NVIDIA DMABUF workaround (gnome-build-meta#754) is applied by the
grep -q org.freedesktop.Platform.GL.nvidia /.flatpak-info && export WEBKIT_DISABLE_DMABUF_RENDERER=1 # binary itself, gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
# Work-around https://github.com/bambulab/BambuStudio/issues/3440 # Work-around https://github.com/bambulab/BambuStudio/issues/3440
# Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues # Use LC_NUMERIC instead of LC_ALL to prevent decimal separator issues
+24 -36
View File
@@ -99,7 +99,6 @@
#include "libslic3r/Preset.hpp" #include "libslic3r/Preset.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/GCode.hpp" #include "libslic3r/GCode.hpp"
#include "libslic3r/MeshRepair.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/ModelArrange.hpp" #include "libslic3r/ModelArrange.hpp"
#include "libslic3r/Platform.hpp" #include "libslic3r/Platform.hpp"
@@ -162,6 +161,7 @@ using json = nlohmann::json;
#include <X11/Xlib.h> #include <X11/Xlib.h>
#endif #endif
#include <unistd.h> #include <unistd.h>
#include "slic3r/GUI/Widgets/WebView.hpp"
#endif #endif
#ifdef SLIC3R_GUI #ifdef SLIC3R_GUI
@@ -207,7 +207,7 @@ std::map<int, std::string> cli_errors = {
{CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."}, {CLI_OBJECT_ORIENT_FAILED, "An error occurred when auto-orienting object(s)."},
{CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."}, {CLI_MODIFIED_PARAMS_TO_PRINTER, "Found modified parameter in printer preset in the 3mf file, which should not be changed."},
{CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."}, {CLI_FILE_VERSION_NOT_SUPPORTED, "Unsupported 3MF version. Please make sure the 3MF file was created with the official version of Bambu Studio, not a beta version."},
{CLI_NO_SUITABLE_OBJECTS, "The plate has nothing to slice: it is empty, or no object on it is both printable and fully inside the print volume."}, {CLI_NO_SUITABLE_OBJECTS, "One of the plate is empty or has no object fully inside it. Please check that the 3mf contains no empty plate in Orca Slicer before uploading."},
{CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."}, {CLI_VALIDATE_ERROR, "There are some incorrect slicing parameters in the 3mf. Please verify the slicing of all plates in Orca Slicer before uploading."},
{CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."}, {CLI_OBJECTS_PARTLY_INSIDE, "Some objects are located over the boundary of the heated bed."},
{CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."}, {CLI_EXPORT_CACHE_DIRECTORY_CREATE_FAILED, "Failed creating directory when exporting cache data."},
@@ -1154,8 +1154,9 @@ int CLI::run(int argc, char **argv)
// opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it // opt-in path. The ~2020-era WebKit2GTK XWayland compositor bug it
// worked around appears fixed in WebKit2GTK >= 2.42; leaving it // worked around appears fixed in WebKit2GTK >= 2.42; leaving it
// unset preserves WebKit hardware acceleration on Device / Setup // unset preserves WebKit hardware acceleration on Device / Setup
// Wizard / login / store. The default path still applies it on // Wizard / login / store. The default path applies it only on
// XWayland sessions as a conservative fallback for older WebKit. // XWayland sessions and only when the linked WebKitGTK predates the
// fix (< 2.42), as a conservative fallback for older WebKit.
// ------------------------------------------------------------------ // ------------------------------------------------------------------
{ {
const char* gdk_backend = ::getenv("GDK_BACKEND"); const char* gdk_backend = ::getenv("GDK_BACKEND");
@@ -1163,6 +1164,14 @@ int CLI::run(int argc, char **argv)
// honours the first backend in the comma-separated list). // honours the first backend in the comma-separated list).
const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11"); const bool x11_opt_in = gdk_backend && boost::starts_with(gdk_backend, "x11");
// NVIDIA's proprietary driver cannot back WebKitGTK's DMABUF renderer.
// Force the shared-memory transport, which keeps compositing and threaded
// scrolling; the second variable clears Debian/Ubuntu/openSUSE's isNVIDIA().
if (::access("/proc/driver/nvidia/version", F_OK) == 0 && WebView::SupportsForceSharedMemory()) {
::setenv("WEBKIT_DMABUF_RENDERER_FORCE_SHM", "1", /* replace */ false);
::setenv("WEBKIT_FORCE_DMABUF_RENDERER", "1", /* replace */ false);
}
if (x11_opt_in) { if (x11_opt_in) {
// ===== X11 / XWayland (opted in via GDK_BACKEND=x11) ===== // ===== X11 / XWayland (opted in via GDK_BACKEND=x11) =====
BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds."; BOOST_LOG_TRIVIAL(info) << "GDK_BACKEND=x11 detected; applying v2.3.2 X11/XWayland workarounds.";
@@ -1206,15 +1215,15 @@ int CLI::run(int argc, char **argv)
} }
#endif #endif
// WebKit2GTK compositing can fail under XWayland on older // WebKit2GTK accelerated compositing could fail under XWayland
// WebKit releases. Disable it only when both DISPLAY and // on WebKit releases before 2.42. Disable it only then, and only
// WAYLAND_DISPLAY are set (i.e. an XWayland session is in // when both DISPLAY and WAYLAND_DISPLAY are set (an XWayland
// use). On pure X11 or native Wayland, compositing is left // session). Disabling it also turns off threaded scrolling, so on
// enabled so WebKit retains HW acceleration. // newer WebKit it only costs performance.
{ {
const char* display_env_wk = ::getenv("DISPLAY"); const char* display_env_wk = ::getenv("DISPLAY");
const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY"); const char* wayland_env_wk = ::getenv("WAYLAND_DISPLAY");
if (display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) { if (WebView::NeedsCompositingWorkaround() && display_env_wk && *display_env_wk && wayland_env_wk && *wayland_env_wk) {
::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false); ::setenv("WEBKIT_DISABLE_COMPOSITING_MODE", "1", /* replace */ false);
} }
} }
@@ -1464,7 +1473,7 @@ int CLI::run(int argc, char **argv)
//int arrange_option; //int arrange_option;
int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1; int plate_to_slice = 0, filament_count = 0, duplicate_count = 0, real_duplicate_count = 0, current_extruder_count = 1, new_extruder_count = 1, current_printer_variant_count = 1, current_print_variant_count = 1, new_printer_variant_count = 1;
bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false; bool first_file = true, is_bbl_3mf = false, need_arrange = true, has_thumbnails = false, up_config_to_date = false, normative_check = true, duplicate_single_object = false, use_first_fila_as_default = false, minimum_save = false, enable_timelapse = false;
bool allow_rotations = false, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false; bool allow_rotations = true, skip_modified_gcodes = false, avoid_extrusion_cali_region = false, skip_useless_pick = false, allow_newer_file = false, current_is_multi_extruder = false, new_is_multi_extruder = false, allow_mix_temp = false, enable_wrapping_detect = false;
Semver file_version; Semver file_version;
std::map<size_t, bool> orients_requirement; std::map<size_t, bool> orients_requirement;
std::vector<Preset*> project_presets; std::vector<Preset*> project_presets;
@@ -5131,19 +5140,9 @@ int CLI::run(int argc, char **argv)
} }
} }
} else if (opt_key == "repair") { } else if (opt_key == "repair") {
if (m_config.opt_bool(opt_key)) // Models are repaired by default.
for (Model &model : m_models) //for (auto &model : m_models)
for (ModelObject *object : model.objects) { // model.repair();
for (ModelVolume *volume : object->volumes) {
MeshRepairStats stats;
std::string error;
if (!volume->repair(true, &stats, &error))
BOOST_LOG_TRIVIAL(warning) << "repair of " << object->name << " / " << volume->name << " failed: " << error;
else if (stats.modified)
BOOST_LOG_TRIVIAL(info) << "repaired " << object->name << " / " << volume->name;
}
object->invalidate_bounding_box();
}
} else { } else {
boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl; boost::nowide::cerr << "error: option not implemented yet: " << opt_key << std::endl;
record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info); record_exit_reson(outfile_dir, CLI_UNSUPPORTED_OPERATION, 0, cli_errors[CLI_UNSUPPORTED_OPERATION], sliced_info);
@@ -5153,7 +5152,6 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n"; BOOST_LOG_TRIVIAL(info) << "finished model pre-process commands\n";
bool oriented_or_arranged = false; bool oriented_or_arranged = false;
bool arranged_without_rotations = false;
//BBS: add orient and arrange logic here //BBS: add orient and arrange logic here
for (auto& model : m_models) for (auto& model : m_models)
{ {
@@ -5421,7 +5419,6 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1); BOOST_LOG_TRIVIAL(info) << boost::format("start plate %1%'s arranging...") % (i + 1);
arrangement::arrange(selected, unselected, beds, arrange_cfg); arrangement::arrange(selected, unselected, beds, arrange_cfg);
//arrangement::arrange(unprintable, {}, beds, arrange_cfg); //arrangement::arrange(unprintable, {}, beds, arrange_cfg);
arranged_without_rotations |= !arrange_cfg.allow_rotations;
BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1); BOOST_LOG_TRIVIAL(info) << boost::format("finished plate %1%'s arranging") % (i + 1);
//step-4: postprocess the bed index and result //step-4: postprocess the bed index and result
@@ -5894,7 +5891,6 @@ int CLI::run(int argc, char **argv)
BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count; BOOST_LOG_TRIVIAL(info) << boost::format("start %1% th arranging...")%arrange_count;
arrangement::arrange(selected, unselected, beds, arrange_cfg); arrangement::arrange(selected, unselected, beds, arrange_cfg);
arrangement::arrange(unprintable, {}, beds, arrange_cfg); arrangement::arrange(unprintable, {}, beds, arrange_cfg);
arranged_without_rotations |= !arrange_cfg.allow_rotations;
BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count; BOOST_LOG_TRIVIAL(info) << boost::format("finished %1% th arranging...")%arrange_count;
//Step-4:postprocess by partplate list&&apply the result //Step-4:postprocess by partplate list&&apply the result
@@ -6439,15 +6435,7 @@ int CLI::run(int argc, char **argv)
if (count == 0) { if (count == 0) {
BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl; BOOST_LOG_TRIVIAL(error) << "plate "<< index+1<< ": Nothing to be sliced, Either the print is empty or no object is fully inside the print volume before apply." << std::endl;
std::string no_objects_reason = cli_errors[CLI_NO_SUITABLE_OBJECTS]; record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, cli_errors[CLI_NO_SUITABLE_OBJECTS], sliced_info);
if (arranged_without_rotations) {
//ORCA: result.json is only written on Linux, so also print the reason to stderr.
no_objects_reason = "No object is fully inside the print volume. Arrange ran without rotations."
" If an object only fits the bed when rotated, pass --allow-rotations."
" Otherwise the plate is empty or an object is larger than the printable area or height.";
boost::nowide::cerr << "plate " << index+1 << ": " << no_objects_reason << std::endl;
}
record_exit_reson(outfile_dir, CLI_NO_SUITABLE_OBJECTS, index+1, no_objects_reason, sliced_info);
flush_and_exit(CLI_NO_SUITABLE_OBJECTS); flush_and_exit(CLI_NO_SUITABLE_OBJECTS);
} }
else if ((plate_to_slice != 0) || pre_check) { else if ((plate_to_slice != 0) || pre_check) {
@@ -429,7 +429,8 @@ if [ "\$XDG_SESSION_TYPE" = "wayland" ] && [ "\$ZINK_DISABLE_OVERRIDE" != "1" ];
export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json export __EGL_VENDOR_LIBRARY_FILENAMES=/usr/share/glvnd/egl_vendor.d/50_mesa.json
export MESA_LOADER_DRIVER_OVERRIDE=zink export MESA_LOADER_DRIVER_OVERRIDE=zink
export GALLIUM_DRIVER=zink export GALLIUM_DRIVER=zink
export WEBKIT_DISABLE_DMABUF_RENDERER=1 # WebKit's NVIDIA DMABUF workaround is applied by the binary itself,
# gated on the runtime WebKitGTK version (src/OrcaSlicer.cpp).
fi fi
fi fi
fi fi
+1 -4
View File
@@ -625,7 +625,6 @@ add_library(libslic3r_cgal STATIC
CutSurface.hpp CutSurface.cpp CutSurface.hpp CutSurface.cpp
IntersectionPoints.hpp IntersectionPoints.cpp IntersectionPoints.hpp IntersectionPoints.cpp
MeshBoolean.hpp MeshBoolean.cpp MeshBoolean.hpp MeshBoolean.cpp
MeshRepair.hpp MeshRepair.cpp
TryCatchSignal.hpp TryCatchSignal.cpp TryCatchSignal.hpp TryCatchSignal.cpp
Triangulation.hpp Triangulation.cpp Triangulation.hpp Triangulation.cpp
) )
@@ -653,9 +652,7 @@ if (IS_CLANG_CL)
target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option) target_compile_options(libslic3r_cgal PRIVATE -Wno-overriding-option)
endif () endif ()
# Lets CGAL algorithms given CGAL::Parallel_if_available_tag run on TBB. target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs)
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_LINKED_WITH_TBB)
target_link_libraries(libslic3r_cgal PRIVATE ${_cgal_tgt} admesh libigl mcut boost_libs TBB::tbb)
if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround if (MSVC AND "${CMAKE_SIZEOF_VOID_P}" STREQUAL "4") # 32 bit MSVC workaround
target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF) target_compile_definitions(libslic3r_cgal PRIVATE CGAL_DO_NOT_USE_MPZF)
+5 -15
View File
@@ -236,29 +236,19 @@ static void getNamedSolids(const TopLoc_Location& location,
int i = 0; int i = 0;
switch (shape_type) { switch (shape_type) {
case TopAbs_COMPOUND: case TopAbs_COMPOUND:
if (!isSplitCompound) {
namedSolids.emplace_back(TopoDS::Compound(transform.Shape()), fullName);
break;
}
case TopAbs_COMPSOLID: case TopAbs_COMPSOLID:
if (!isSplitCompound) { if (!isSplitCompound) {
namedSolids.emplace_back(transform.Shape(), fullName); namedSolids.emplace_back(TopoDS::CompSolid(transform.Shape()), fullName);
} else { } else {
for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) { for (explorer.Init(transform.Shape(), TopAbs_SOLID); explorer.More(); explorer.Next()) {
i++; i++;
const TopoDS_Shape& currentShape = explorer.Current(); const TopoDS_Shape& currentShape = explorer.Current();
namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i)); namedSolids.emplace_back(TopoDS::Solid(currentShape), fullName + "-SOLID-" + std::to_string(i));
} }
// Surface models have no solids: each shell outside a solid is a part, and loose faces stay together.
i = 0;
for (explorer.Init(transform.Shape(), TopAbs_SHELL, TopAbs_SOLID); explorer.More(); explorer.Next())
namedSolids.emplace_back(explorer.Current(), fullName + "-SHELL-" + std::to_string(++i));
TopoDS_Compound loose_faces;
TopoDS_Builder builder;
builder.MakeCompound(loose_faces);
bool has_loose_faces = false;
for (explorer.Init(transform.Shape(), TopAbs_FACE, TopAbs_SHELL); explorer.More(); explorer.Next()) {
builder.Add(loose_faces, explorer.Current());
has_loose_faces = true;
}
if (has_loose_faces)
namedSolids.emplace_back(loose_faces, fullName);
} }
break; break;
case TopAbs_SOLID: case TopAbs_SOLID:
+4 -168
View File
@@ -22,7 +22,6 @@
#include "libslic3r/BrimEarsPoint.hpp" #include "libslic3r/BrimEarsPoint.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/CustomGCode.hpp" #include "libslic3r/CustomGCode.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r_version.h" #include "libslic3r_version.h"
#include "bbs_3mf.hpp" #include "bbs_3mf.hpp"
@@ -355,13 +354,6 @@ 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_FUZZY_SKIN_ATTR = "paint_fuzzy_skin";
static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam"; static constexpr const char* CUSTOM_SEAM_ATTR = "paint_seam";
static constexpr const char* MMU_SEGMENTATION_ATTR = "paint_color"; 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 // BBS
static constexpr const char* FACE_PROPERTY_ATTR = "face_property"; static constexpr const char* FACE_PROPERTY_ATTR = "face_property";
@@ -855,9 +847,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
std::vector<std::string> custom_seam; std::vector<std::string> custom_seam;
std::vector<std::string> mmu_segmentation; std::vector<std::string> mmu_segmentation;
std::vector<std::string> fuzzy_skin; 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 // BBS
std::vector<std::string> face_properties; std::vector<std::string> face_properties;
@@ -878,8 +867,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
custom_seam.clear(); custom_seam.clear();
mmu_segmentation.clear(); mmu_segmentation.clear();
fuzzy_skin.clear(); fuzzy_skin.clear();
for (std::vector<std::string> &slot : texture_displacement)
slot.clear();
} }
}; };
@@ -1213,11 +1200,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
/*IdToSlaSupportPointsMap m_sla_support_points; /*IdToSlaSupportPointsMap m_sla_support_points;
IdToSlaDrainHolesMap m_sla_drain_holes;*/ IdToSlaDrainHolesMap m_sla_drain_holes;*/
PathToEmbossShapeFileMap m_path_to_emboss_shape_files; 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_metadata_name;
std::string m_curr_characters; std::string m_curr_characters;
std::string m_name; std::string m_name;
@@ -1747,8 +1729,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
//plate->pattern_file = it->second->pattern_file; //plate->pattern_file = it->second->pattern_file;
plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file; plate->no_light_thumbnail_file = it->second->no_light_thumbnail_file;
plate->top_file = it->second->top_file; plate->top_file = it->second->top_file;
plate->pick_file = it->second->pick_file; plate->pick_file = it->second->pick_file.empty();
plate->pattern_bbox_file = it->second->pattern_bbox_file; plate->pattern_bbox_file = it->second->pattern_bbox_file.empty();
plate->config = it->second->config; plate->config = it->second->config;
if (!plate->thumbnail_file.empty()) if (!plate->thumbnail_file.empty())
@@ -2091,13 +2073,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
else if (_is_svg_shape_file(name)) { else if (_is_svg_shape_file(name)) {
_extract_embossed_svg_shape_file(name, archive, stat); _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)) { else if (!dont_load_config && boost::algorithm::iequals(name, SLICE_INFO_CONFIG_FILE)) {
m_parsing_slice_info = true; m_parsing_slice_info = true;
//extract slice info from archive //extract slice info from archive
@@ -3335,45 +3310,6 @@ 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){ 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()); 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'); auto file = std::make_unique<std::string>(stat.m_uncomp_size, '\0');
@@ -4030,12 +3966,6 @@ 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.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.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)); 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 // BBS
m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); m_curr_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
} }
@@ -5387,18 +5317,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->mmu_segmentation_facets.touch(); volume->mmu_segmentation_facets.touch();
volume->fuzzy_skin_facets.shrink_to_fit(); volume->fuzzy_skin_facets.shrink_to_fit();
volume->fuzzy_skin_facets.touch(); 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); volume->set_type(volume_data->part_type);
@@ -5414,8 +5332,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
for (const Metadata& metadata : volume_data->metadata) { for (const Metadata& metadata : volume_data->metadata) {
if (metadata.key == NAME_KEY) if (metadata.key == NAME_KEY)
volume->name = metadata.value; 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")) //else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); // volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format //for old format
@@ -5580,28 +5496,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
volume->seam_facets.shrink_to_fit(); volume->seam_facets.shrink_to_fit();
volume->mmu_segmentation_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); volume->set_type(volume_data.part_type);
// apply the remaining volume's metadata // apply the remaining volume's metadata
for (const Metadata& metadata : volume_data.metadata) { for (const Metadata& metadata : volume_data.metadata) {
if (metadata.key == NAME_KEY) if (metadata.key == NAME_KEY)
volume->name = metadata.value; 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")) //else if ((metadata.key == MODIFIER_KEY) && (metadata.value == "1"))
// volume->set_type(ModelVolumeType::PARAMETER_MODIFIER); // volume->set_type(ModelVolumeType::PARAMETER_MODIFIER);
//for old format //for old format
@@ -5905,12 +5805,6 @@ 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.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.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)); 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 // BBS
current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR)); current_object->geometry.face_properties.push_back(bbs_get_attribute_value_string(attributes, num_attributes, FACE_PROPERTY_ATTR));
} }
@@ -6914,9 +6808,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
stream << " <Default Extension=\"rels\" ContentType=\"application/vnd.openxmlformats-package.relationships+xml\"/>\n"; 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=\"model\" ContentType=\"application/vnd.ms-package.3dmanufacturing-3dmodel+xml\"/>\n";
stream << " <Default Extension=\"png\" ContentType=\"image/png\"/>\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 << " <Default Extension=\"gcode\" ContentType=\"text/x.gcode\"/>\n";
stream << "</Types>"; stream << "</Types>";
@@ -7341,14 +7232,10 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (iter != m_shared_meshes.end()) if (iter != m_shared_meshes.end())
{ {
const ModelVolume* shared_volume = iter->second.second; 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)) if ((shared_volume->supported_facets.equals(volume->supported_facets))
&& (shared_volume->seam_facets.equals(volume->seam_facets)) && (shared_volume->seam_facets.equals(volume->seam_facets))
&& (shared_volume->mmu_segmentation_facets.equals(volume->mmu_segmentation_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; 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}}); const_cast<_BBS_3MF_Exporter *>(this)->m_volume_paths.insert({volume, {data->sub_path, data->volumes_objectID.find(iter->second.second)->second}});
@@ -7764,20 +7651,6 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
output_buffer += "\""; 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 // BBS
if (i < its.properties.size()) { if (i < its.properties.size()) {
std::string prop_str = its.properties[i].to_string(); std::string prop_str = its.properties[i].to_string();
@@ -8227,42 +8100,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return file_path; return file_path;
} }
// Writes a volume's texture displacement stack into the archive and points the volume's metadata at it. 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)
// 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; std::stringstream stream;
// Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back // Store mesh transformation in full precision, as the volumes are stored transformed and they need to be transformed back
@@ -8374,8 +8212,6 @@ bool _BBS_3MF_Exporter::_add_model_config_file_to_archive(mz_zip_archive& archiv
to_xml(stream, *es, *volume, archive, m_fullpath_sources); 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; if (const std::optional<TextConfiguration> &tc = volume->text_configuration;
tc.has_value()) tc.has_value())
TextConfigurationSerialization::to_xml(stream, *tc); TextConfigurationSerialization::to_xml(stream, *tc);
+3 -16
View File
@@ -1033,15 +1033,6 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return buf; return buf;
} }
// FanMover consumes these markers (and drops them) to avoid splitting a G1 inside custom gcode.
// Only written when FanMover runs, the same gate as its construction in GCode::process_layers.
static std::string wrap_custom_gcode_for_fan_mover(const FullPrintConfig &config, const std::string &gcode)
{
if (config.fan_speedup_time.value == 0 && config.fan_kickstart.value <= 0)
return gcode;
return ";_FAN_MOVER_SKIP_START\n" + gcode + ";_FAN_MOVER_SKIP_END\n";
}
std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const std::string WipeTowerIntegration::append_tcr(GCode& gcodegen, const WipeTower::ToolChangeResult& tcr, int new_filament_id, double z) const
{ {
if (new_filament_id != -1 && new_filament_id != tcr.new_tool) if (new_filament_id != -1 && new_filament_id != tcr.new_tool)
@@ -1393,9 +1384,6 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config); toolchange_gcode_str = gcodegen.placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &config);
check_add_eol(toolchange_gcode_str); check_add_eol(toolchange_gcode_str);
// Type1 wipe tower path (every BBL printer): bracket the same way set_extruder() does,
// so FanMover doesn't split a G1 inside this custom gcode either.
toolchange_gcode_str = wrap_custom_gcode_for_fan_mover(gcodegen.config(), toolchange_gcode_str);
//BBS //BBS
{ {
@@ -3307,7 +3295,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
m_is_role_based_fan_on.fill(false); m_is_role_based_fan_on.fill(false);
m_role_based_fan_marker_layer.fill(-1); m_role_based_fan_marker_layer.fill(-1);
m_fan_mover.reset(); m_fan_mover.release();
m_ordering_cache.clear(); m_ordering_cache.clear();
// Belt printer: initialize belt-specific writer via virtual hook. // Belt printer: initialize belt-specific writer via virtual hook.
@@ -4262,7 +4250,7 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get()))); config.set_key_value("nozzle_diameter_at_nozzle_id", new ConfigOptionFloats(get_nozzle_diameters_by_nozzle_id(group_result.get())));
config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get()))); config.set_key_value("nozzle_volume_types", new ConfigOptionStrings(get_nozzle_volume_types_by_nozzle_id(group_result.get())));
config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index)); config.set_key_value("layer_num", new ConfigOptionInt(m_layer_index));
std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.get_at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config); std::string filament_start_gcode = this->placeholder_parser_process("filament_start_gcode", print.config().filament_start_gcode.values.at(initial_non_support_extruder_id), initial_non_support_extruder_id,&config);
file.writeln(filament_start_gcode); file.writeln(filament_start_gcode);
// Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where // Mark the first filament used in print. Multi-nozzle printers (H2C) get ";VT%d H%d" where
// H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their // H = dynamic ? nozzle_id : -1; existing single-nozzle printers keep the bare ";VT%d" so their
@@ -10907,8 +10895,7 @@ std::string GCode::set_extruder(unsigned int new_filament_id, double print_z, bo
toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config); toolchange_gcode_parsed = placeholder_parser_process("change_filament_gcode", change_filament_gcode, new_filament_id, &dyn_config);
check_add_eol(toolchange_gcode_parsed); check_add_eol(toolchange_gcode_parsed);
// toolchange_gcode_parsed is still read below (get_last_z_from_gcode, custom_gcode_changes_tool). gcode += toolchange_gcode_parsed;
gcode += wrap_custom_gcode_for_fan_mover(m_config, toolchange_gcode_parsed);
//BBS //BBS
{ {
-5
View File
@@ -445,11 +445,6 @@ void FanMover::_process_gcode_line(GCodeReader& reader, const GCodeReader::GCode
std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6); std::string extrusion_string = line.raw().substr(6, line.raw().size() - 6);
current_role = ExtrusionEntity::string_to_role(extrusion_string); current_role = ExtrusionEntity::string_to_role(extrusion_string);
} }
if (line.raw().rfind(";_FAN_MOVER_SKIP_", 0) == 0) {
// Internal markers: read here, then dropped from the output.
m_is_custom_gcode = line.raw().rfind(";_FAN_MOVER_SKIP_START", 0) == 0;
time = -1;
}
if (line.raw().size() > 16) { if (line.raw().size() > 16) {
if (line.raw().rfind("; custom gcode", 0) != std::string::npos) { if (line.raw().rfind("; custom gcode", 0) != std::string::npos) {
if (line.raw().rfind("; custom gcode end", 0) != std::string::npos) if (line.raw().rfind("; custom gcode end", 0) != std::string::npos)
+2 -7
View File
@@ -2602,13 +2602,12 @@ void GCodeProcessorResult::reset() {
//BBS: add mutex for protection of gcode result //BBS: add mutex for protection of gcode result
lock(); lock();
// moves and lines_ends grow with the print: free them, as no reset result reuses the capacity. moves.clear();
clear_and_shrink(moves);
plate_mass = {}; plate_mass = {};
object_masses.clear(); object_masses.clear();
body_masses.clear(); body_masses.clear();
support_masses.clear(); support_masses.clear();
clear_and_shrink(lines_ends); lines_ends.clear();
printable_area = Pointfs(); printable_area = Pointfs();
//BBS: add bed exclude area //BBS: add bed exclude area
bed_exclude_area = Pointfs(); bed_exclude_area = Pointfs();
@@ -3599,10 +3598,6 @@ void GCodeProcessor::apply_config(const DynamicPrintConfig& config)
if (has_scarf_joint_seam != nullptr) if (has_scarf_joint_seam != nullptr)
m_detect_layer_based_on_tag = m_detect_layer_based_on_tag || has_scarf_joint_seam->value; m_detect_layer_based_on_tag = m_detect_layer_based_on_tag || has_scarf_joint_seam->value;
const ConfigOptionBool* manual_filament_change = config.option<ConfigOptionBool>("manual_filament_change");
if (manual_filament_change != nullptr)
m_manual_filament_change = manual_filament_change->value;
const ConfigOptionEnumGeneric *bed_type = config.option<ConfigOptionEnumGeneric>("curr_bed_type"); const ConfigOptionEnumGeneric *bed_type = config.option<ConfigOptionEnumGeneric>("curr_bed_type");
if (bed_type != nullptr) if (bed_type != nullptr)
m_result.bed_type = (BedType)bed_type->value; m_result.bed_type = (BedType)bed_type->value;
+1 -1
View File
@@ -1228,7 +1228,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
// m_result.nozzle_group_result, which is handed over only after the stream for the // m_result.nozzle_group_result, which is handed over only after the stream for the
// pre-heat injector's second pass and gates the richer change-time model. // pre-heat injector's second pass and gates the richer change-time model.
std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result; std::shared_ptr<MultiNozzleUtils::NozzleGroupResultBase> m_nozzle_group_result;
bool m_manual_filament_change{ false }; bool m_manual_filament_change;
//BBS: x, y offset for gcode generated //BBS: x, y offset for gcode generated
double m_x_offset{ 0 }; double m_x_offset{ 0 };
+349
View File
@@ -0,0 +1,349 @@
#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
@@ -0,0 +1,39 @@
#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_ */
+88
View File
@@ -29,6 +29,8 @@
#include <CGAL/Polygon_mesh_processing/triangulate_hole.h> #include <CGAL/Polygon_mesh_processing/triangulate_hole.h>
#include <CGAL/Polygon_mesh_processing/stitch_borders.h> #include <CGAL/Polygon_mesh_processing/stitch_borders.h>
#include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h> #include <CGAL/Polygon_mesh_processing/polygon_soup_to_polygon_mesh.h>
#include <CGAL/Polygon_mesh_processing/repair_degeneracies.h>
#include <CGAL/Polygon_mesh_processing/manifoldness.h>
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <algorithm> #include <algorithm>
@@ -48,6 +50,7 @@
#include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h> #include <CGAL/Polygon_mesh_processing/orient_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/repair.h> #include <CGAL/Polygon_mesh_processing/repair.h>
#include <CGAL/Polygon_mesh_processing/remesh.h> #include <CGAL/Polygon_mesh_processing/remesh.h>
#include <CGAL/Polygon_mesh_processing/repair_polygon_soup.h>
#include <CGAL/Polygon_mesh_processing/orientation.h> #include <CGAL/Polygon_mesh_processing/orientation.h>
// BBS: for segment // BBS: for segment
#include <CGAL/mesh_segmentation.h> #include <CGAL/mesh_segmentation.h>
@@ -655,6 +658,91 @@ bool empty(const CGALMesh &mesh)
return mesh.m.is_empty(); return mesh.m.is_empty();
} }
bool repair(TriangleMesh& mesh, RepairedMeshErrors* repaired_errors, std::string* error)
{
namespace PMP = CGAL::Polygon_mesh_processing;
if (mesh.empty())
return true;
try {
// 1) Convert to polygon soup
std::vector<_EpicMesh::Point> points;
std::vector<std::vector<std::size_t>> polygons;
points.reserve(mesh.its.vertices.size());
polygons.reserve(mesh.its.indices.size());
for (const auto& v : mesh.its.vertices)
points.emplace_back(v.x(), v.y(), v.z());
for (const auto& f : mesh.its.indices)
polygons.push_back({size_t(f[0]), size_t(f[1]), size_t(f[2])});
// 2) Aggressive soup cleanup
PMP::repair_polygon_soup(points, polygons);
// 3) Convert soup → mesh
_EpicMesh cgal_mesh;
PMP::polygon_soup_to_polygon_mesh(points, polygons, cgal_mesh);
// 4) Remove degenerate geometry
PMP::remove_degenerate_faces(cgal_mesh);
PMP::remove_isolated_vertices(cgal_mesh);
// 5) Fix remaining non-manifold vertices
PMP::duplicate_non_manifold_vertices(cgal_mesh);
// 6) Boolean union (keeps only outer shell)
_EpicMesh tmp;
if (PMP::corefine_and_compute_union(cgal_mesh, cgal_mesh, tmp)) {
cgal_mesh = std::move(tmp);
}
// If it fails, continue anyway with previous mesh
// 7) Fill holes
if (!CGAL::is_closed(cgal_mesh)) {
using halfedge_descriptor = boost::graph_traits<_EpicMesh>::halfedge_descriptor;
std::vector<halfedge_descriptor> borders;
CGAL::extract_boundary_cycles(cgal_mesh, std::back_inserter(borders));
for (halfedge_descriptor h : borders) {
PMP::triangulate_and_refine_hole(cgal_mesh, h);
}
}
// 8) Final validity check
if (!CGAL::is_closed(cgal_mesh)) {
if (error)
*error = "Repair failed: mesh still open after hole filling.";
return false;
}
// 9) Ensure outward orientation
if (!PMP::does_bound_a_volume(cgal_mesh))
PMP::orient_to_bound_a_volume(cgal_mesh);
// 10) Convert back
indexed_triangle_set its = cgal_to_indexed_triangle_set(cgal_mesh);
RepairedMeshErrors errs{};
errs.facets_removed = 0;
errs.edges_fixed = 0;
mesh = TriangleMesh(std::move(its), errs);
if (repaired_errors)
*repaired_errors = errs;
return true;
} catch (const std::exception& e) {
if (error)
*error = e.what();
return false;
}
}
CGALMeshPtr clone(const CGALMesh &m) CGALMeshPtr clone(const CGALMesh &m)
{ {
return CGALMeshPtr{new CGALMesh{m}}; return CGALMeshPtr{new CGALMesh{m}};
+3
View File
@@ -77,6 +77,9 @@ TriangleMesh merge(std::vector<TriangleMesh> meshes);
bool does_bound_a_volume(const CGALMesh &mesh); bool does_bound_a_volume(const CGALMesh &mesh);
bool empty(const CGALMesh &mesh); bool empty(const CGALMesh &mesh);
// Repair a mesh using CGAL. Returns true on success. Optionally returns a summary of repairs and an error string.
bool repair(TriangleMesh &mesh, RepairedMeshErrors *repaired_errors = nullptr, std::string *error = nullptr);
// Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface // Real UV unwrap of an open mesh patch via CGAL's LSCM (Least Squares Conformal Maps) surface
// parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`), // parameterization. Returns one UV coordinate per input vertex (same indexing as `mesh.vertices`),
// or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected // or nullopt if `mesh` isn't a single topological disk -- LSCM needs exactly one connected
File diff suppressed because it is too large Load Diff
-28
View File
@@ -1,28 +0,0 @@
#pragma once
#include <cstddef>
#include <functional>
#include <string>
namespace Slic3r {
class TriangleMesh;
struct MeshRepairStats
{
std::size_t welded_vertices = 0;
std::size_t removed_triangles = 0;
std::size_t reversed_shells = 0;
std::size_t filled_holes = 0;
std::size_t removed_shells = 0;
std::size_t self_intersections = 0;
// False when the input already was closed, consistently oriented and free of self-intersections.
bool modified = false;
};
// Returns false, leaving the mesh untouched, if no solid can be built. progress gets the percentage done and may
// throw to cancel. See docs/HLSD/mesh-repair.md.
bool repair_mesh(TriangleMesh &mesh, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
const std::function<void(int)> &progress = {});
} // namespace Slic3r
+22 -39
View File
@@ -29,7 +29,6 @@
#include "TriangleMeshSlicer.hpp" #include "TriangleMeshSlicer.hpp"
#include "TriangleSelector.hpp" #include "TriangleSelector.hpp"
#include "MaterialType.hpp" #include "MaterialType.hpp"
#include "MeshRepair.hpp"
#include "Format/AMF.hpp" #include "Format/AMF.hpp"
#include "Format/svg.hpp" #include "Format/svg.hpp"
@@ -2947,18 +2946,6 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
if (text_configuration.has_value()) if (text_configuration.has_value())
text_configuration.reset(); text_configuration.reset();
// Parts without a convex hull enclose no volume; drop them here, as deleting volumes later could delete this one.
std::vector<std::unique_ptr<ModelVolume>> parts;
for (TriangleMesh &mesh : meshes)
if (!mesh.empty()) {
std::unique_ptr<ModelVolume> part(new ModelVolume(object, *this, std::move(mesh)));
if (const std::shared_ptr<const TriangleMesh> &hull = part->get_convex_hull_shared_ptr();
hull != nullptr && !hull->its.vertices.empty() && !hull->its.indices.empty())
parts.emplace_back(std::move(part));
}
if (parts.empty())
return 1;
std::optional<TriangleSelector::SavedPainting> saved_painting = remap_paint ? save_painting() : std::optional<TriangleSelector::SavedPainting> saved_painting = remap_paint ? save_painting() :
std::optional<TriangleSelector::SavedPainting>{}; std::optional<TriangleSelector::SavedPainting>{};
@@ -2969,22 +2956,26 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
unsigned int extruder_counter = 0; unsigned int extruder_counter = 0;
const Vec3d offset = this->get_offset(); const Vec3d offset = this->get_offset();
for (std::unique_ptr<ModelVolume> &part : parts) { for (TriangleMesh &mesh : meshes) {
if (mesh.empty())
// Repair may have removed unconnected triangles, thus emptying the mesh.
continue;
if (idx == 0) { if (idx == 0) {
this->set_mesh(part->m_mesh); this->set_mesh(std::move(mesh));
m_convex_hull = part->m_convex_hull; this->calculate_convex_hull();
this->invalidate_convex_hull_2d(); this->invalidate_convex_hull_2d();
// Assign a new unique ID, so that a new GLVolume will be generated. // Assign a new unique ID, so that a new GLVolume will be generated.
this->set_new_unique_id(); this->set_new_unique_id();
// reset the source to disable reload from disk
this->source = ModelVolume::Source();
// BBS: reset facet annotations // BBS: reset facet annotations
this->reset_extra_facets(); this->reset_extra_facets();
} }
else else
this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), part.release()); this->object->volumes.insert(this->object->volumes.begin() + (++ivolume), new ModelVolume(object, *this, std::move(mesh)));
// reset the source to disable reload from disk
this->object->volumes[ivolume]->source = ModelVolume::Source();
this->object->volumes[ivolume]->set_offset(Vec3d::Zero()); this->object->volumes[ivolume]->set_offset(Vec3d::Zero());
this->object->volumes[ivolume]->center_geometry_after_creation(); this->object->volumes[ivolume]->center_geometry_after_creation();
this->object->volumes[ivolume]->translate(offset); this->object->volumes[ivolume]->translate(offset);
@@ -2998,29 +2989,21 @@ size_t ModelVolume::split(unsigned int max_extruders, bool remap_paint)
++ idx; ++ idx;
} }
// discard volumes for which the convex hull was not generated or is degenerate
size_t i = 0;
while (i < this->object->volumes.size()) {
const std::shared_ptr<const TriangleMesh> &hull = this->object->volumes[i]->get_convex_hull_shared_ptr();
if (hull == nullptr || hull->its.vertices.empty() || hull->its.indices.empty()) {
this->object->delete_volume(i);
--idx;
--i;
}
++i;
}
return idx; return idx;
} }
bool ModelVolume::repair(bool keep_painting, MeshRepairStats *stats_out, std::string *error, const std::function<void(int)> &progress)
{
TriangleMesh mesh = this->mesh();
MeshRepairStats stats;
const bool ok = repair_mesh(mesh, &stats, error, progress);
if (stats_out)
*stats_out = stats;
if (!ok || !stats.modified)
return ok;
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ? save_painting() : std::nullopt;
this->set_mesh(std::move(mesh));
this->calculate_convex_hull();
this->invalidate_convex_hull_2d();
// Assign a new unique ID, so that a new GLVolume will be generated.
this->set_new_unique_id();
this->restore_painting(saved_painting);
return true;
}
void ModelVolume::translate(const Vec3d& displacement) void ModelVolume::translate(const Vec3d& displacement)
{ {
set_offset(get_offset() + displacement); set_offset(get_offset() + displacement);
+7 -11
View File
@@ -77,7 +77,6 @@ class Preset;
class BBLProject; class BBLProject;
class KeyStore; class KeyStore;
struct MeshRepairStats;
namespace UndoRedo { namespace UndoRedo {
class StackImpl; class StackImpl;
@@ -888,13 +887,13 @@ public:
// The triangular model. // The triangular model.
const TriangleMesh& mesh() const { return *m_mesh.get(); } const TriangleMesh& mesh() const { return *m_mesh.get(); }
std::shared_ptr<const TriangleMesh> mesh_ptr() const { return m_mesh; } std::shared_ptr<const TriangleMesh> mesh_ptr() const { return m_mesh; }
void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; } void set_mesh(const TriangleMesh &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); }
void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; } void set_mesh(TriangleMesh &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); }
void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); m_is_splittable = -1; } void set_mesh(const indexed_triangle_set &mesh) { m_mesh = std::make_shared<const TriangleMesh>(mesh); }
void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); m_is_splittable = -1; } void set_mesh(indexed_triangle_set &&mesh) { m_mesh = std::make_shared<const TriangleMesh>(std::move(mesh)); }
void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; m_is_splittable = -1; } void set_mesh(std::shared_ptr<const TriangleMesh> &mesh) { m_mesh = mesh; }
void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); m_is_splittable = -1; } void set_mesh(std::unique_ptr<const TriangleMesh> &&mesh) { m_mesh = std::move(mesh); }
void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); m_is_splittable = -1; } void reset_mesh() { m_mesh = std::make_shared<const TriangleMesh>(); }
const std::shared_ptr<const TriangleMesh>& get_mesh_shared_ptr() const { return m_mesh; } const std::shared_ptr<const TriangleMesh>& get_mesh_shared_ptr() const { return m_mesh; }
// Configuration parameters specific to an object model geometry or a modifier volume, // Configuration parameters specific to an object model geometry or a modifier volume,
// overriding the global Slic3r settings and the ModelObject settings. // overriding the global Slic3r settings and the ModelObject settings.
@@ -1044,9 +1043,6 @@ public:
// Return the number of volumes created from this one. // Return the number of volumes created from this one.
// This is useful to assign different materials to different volumes of an object. // This is useful to assign different materials to different volumes of an object.
size_t split(unsigned int max_extruders, bool remap_paint); size_t split(unsigned int max_extruders, bool remap_paint);
// Replaces the mesh by its repair_mesh() result if that changed it. False if no solid can be built.
bool repair(bool keep_painting, MeshRepairStats *stats = nullptr, std::string *error = nullptr,
const std::function<void(int)> &progress = {});
void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); } void translate(double x, double y, double z) { translate(Vec3d(x, y, z)); }
void translate(const Vec3d& displacement); void translate(const Vec3d& displacement);
void scale(const Vec3d& scaling_factors); void scale(const Vec3d& scaling_factors);
+6 -35
View File
@@ -2164,16 +2164,6 @@ int PresetCollection::get_differed_values_to_update(Preset& preset, std::map<std
DynamicPrintConfig temp_config; DynamicPrintConfig temp_config;
std::vector<std::string> dirty_options = preset.config.diff(parent_preset->config); std::vector<std::string> dirty_options = preset.config.diff(parent_preset->config);
// Send the variant layout even when it matches the parent's, as the local save does: the loader
// sizes the per-variant values by it and would otherwise keep only the first one.
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
if (!extruder_id_name.empty())
dirty_options.emplace_back(extruder_id_name);
if (!extruder_variant_name.empty())
dirty_options.emplace_back(extruder_variant_name);
for (auto option: dirty_options) for (auto option: dirty_options)
{ {
ConfigOption *opt_src = preset.config.option(option); ConfigOption *opt_src = preset.config.option(option);
@@ -2638,34 +2628,15 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
new_config = default_preset.config; new_config = default_preset.config;
} }
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
// Older uploads left out a variant layout equal to the parent's but still sent one value per
// parent variant. Read those values against the parent's layout instead of cutting them to one.
if (inherit_preset && !cloud_config.has(extruder_variant_name)) {
const auto *parent_variants = inherit_preset->config.option<ConfigOptionStrings>(extruder_variant_name);
const size_t variant_count = parent_variants ? parent_variants->size() : 0;
bool values_per_variant = false;
for (const std::string &key : cloud_config.keys()) {
const size_t stride = key_set2->count(key) ? 2 : key_set1->count(key) ? 1 : 0;
const auto *values = dynamic_cast<const ConfigOptionVectorBase *>(cloud_config.option(key));
if (stride > 0 && key != extruder_id_name && values && variant_count > 1 && values->size() == variant_count * stride) {
values_per_variant = true;
break;
}
}
if (values_per_variant)
for (const std::string &key : {extruder_id_name, extruder_variant_name})
if (!key.empty() && !cloud_config.has(key) && inherit_preset->config.has(key))
cloud_config.set_key_value(key, inherit_preset->config.option(key)->clone());
}
extend_default_config_length(cloud_config, false, {}); extend_default_config_length(cloud_config, false, {});
if (inherit_preset) if (inherit_preset) {
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
new_config.update_diff_values_to_child_config(cloud_config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2); new_config.update_diff_values_to_child_config(cloud_config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
else{ else{
new_config.apply(std::move(cloud_config)); new_config.apply(std::move(cloud_config));
extend_default_config_length(new_config, true, default_preset.config); extend_default_config_length(new_config, true, default_preset.config);
-9
View File
@@ -2991,15 +2991,6 @@ void PrintObject::clear_shared_object()
void PrintObject::copy_layers_from_shared_object() void PrintObject::copy_layers_from_shared_object()
{ {
if (m_shared_object) { if (m_shared_object) {
// Orca: Print::process() calls this once per slice for each sharing object, after clear_shared_object() has
// emptied any earlier alias, so the vectors hold only layers this object sliced itself before it started sharing
// (or nothing). Free them here, as clear_support_layers() / clear_layers() would; those skip deletion while
// m_shared_object is set. The belt-only data they also free is always empty here, because belt printers never
// share layers.
for (SupportLayer *layer : m_support_layers)
delete layer;
for (Layer *layer : m_layers)
delete layer;
m_layers.clear(); m_layers.clear();
m_support_layers.clear(); m_support_layers.clear();
+4 -5
View File
@@ -12866,11 +12866,10 @@ CLITransformConfigDef::CLITransformConfigDef()
//def->cli = "orient|o"; //def->cli = "orient|o";
def->set_default_value(new ConfigOptionInt(0)); def->set_default_value(new ConfigOptionInt(0));
def = this->add("repair", coBool); /*def = this->add("repair", coBool);
def->label = L("Repair"); def->label = L("Repair");
def->tooltip = L("Repair the meshes of the models as Fix Model does: close holes and cracks, orient them outward and " def->tooltip = L("Repair the model's meshes if it is non-manifold mesh.");
"remove self-intersections."); def->set_default_value(new ConfigOptionBool(false));*/
def->set_default_value(new ConfigOptionBool(false));
def = this->add("rotate", coFloat); def = this->add("rotate", coFloat);
def->label = L("Rotate"); def->label = L("Rotate");
@@ -13086,7 +13085,7 @@ CLIMiscConfigDef::CLIMiscConfigDef()
def = this->add("allow_rotations", coBool); def = this->add("allow_rotations", coBool);
def->label = L("Allow rotation when arranging"); def->label = L("Allow rotation when arranging");
def->tooltip = L("If enabled, Arrange will allow rotation when placing objects."); def->tooltip = L("If enabled, Arrange will allow rotation when placing objects.");
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(true));
def = this->add("align_to_y_axis", coBool); def = this->add("align_to_y_axis", coBool);
def->label = L("Align to Y axis when arranging"); def->label = L("Align to Y axis when arranging");
-3
View File
@@ -982,9 +982,6 @@ void PrintObject::generate_support_material(bool with_belt_brim)
this->_generate_support_material(); this->_generate_support_material();
m_print->throw_if_canceled(); m_print->throw_if_canceled();
} }
// Orca: the tree support collision/avoidance caches and support nodes are only used while this step runs
// (detect_overhangs() rebuilds them from scratch), so don't keep them resident until the next slice.
this->clear_tree_support_preview_cache();
// Belt brim rides here rather than in the brim step because its apron // Belt brim rides here rather than in the brim step because its apron
// prologue introduces print_z values below the object's first layer, and // prologue introduces print_z values below the object's first layer, and
// those must exist before ToolOrdering is built at psWipeTower - one step // those must exist before ToolOrdering is built at psWipeTower - one step
+1 -1
View File
@@ -1532,7 +1532,7 @@ void PrintObject::slice_volumes()
if (min_growth < 0.f || elfoot > 0.f) { if (min_growth < 0.f || elfoot > 0.f) {
// Apply the negative XY compensation. (the ones that is <0) // Apply the negative XY compensation. (the ones that is <0)
ExPolygons trimming; ExPolygons trimming;
const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5); static const float eps = float(scale_(m_config.slice_closing_radius.value) * 1.5);
if (elfoot > 0.f) { if (elfoot > 0.f) {
ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps); ExPolygons expolygons_to_compensate = offset_ex(layer->merged(eps), -eps);
lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate; lslices_elfoot_uncompensated[layer_id] = expolygons_to_compensate;
+30 -13
View File
@@ -1976,15 +1976,37 @@ coordf_t TreeSupport::get_radius(const SupportNode* node)
return node->radius; return node->radius;
} }
// Orca: drop_nodes() calls these several times per node per layer, so they return a reference into the ExPolygons TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
// TreeSupportData cache. The cache only ever inserts, so its entries never move while nodes drop in parallel.
const ExPolygons& TreeSupport::get_avoidance(coordf_t radius, size_t obj_layer_nr)
{ {
#if USE_SUPPORT_3D
if (m_model_volumes) {
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
const Polygons& avoid_polys = m_model_volumes->getAvoidance(radius, obj_layer_nr, TreeSupport3D::TreeModelVolumes::AvoidanceType::FastSafe, on_build_plate, true);
ExPolygons expolys;
for (auto& poly : avoid_polys)
expolys.emplace_back(std::move(poly));
return expolys;
}
return ExPolygons();
#else
return m_ts_data->get_avoidance(radius, obj_layer_nr); return m_ts_data->get_avoidance(radius, obj_layer_nr);
#endif
} }
const ExPolygons& TreeSupport::get_collision(coordf_t radius, size_t layer_nr) ExPolygons TreeSupport::get_collision(coordf_t radius, size_t layer_nr)
{ {
#if USE_SUPPORT_3D
if (m_model_volumes) {
bool on_build_plate = m_object_config->support_on_build_plate_only.value;
const Polygons& collision_polys = m_model_volumes->getCollision(radius, layer_nr, true);
ExPolygons expolys;
for (auto& poly : collision_polys)
expolys.emplace_back(std::move(poly));
return expolys;
}
#else
return m_ts_data->get_collision(radius, layer_nr); return m_ts_data->get_collision(radius, layer_nr);
#endif
return ExPolygons();
} }
Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr) Polygons TreeSupport::get_collision_polys(coordf_t radius, size_t layer_nr)
{ {
@@ -2805,12 +2827,7 @@ void TreeSupport::draw_circles()
#endif // SUPPORT_TREE_DEBUG_TO_SVG #endif // SUPPORT_TREE_DEBUG_TO_SVG
SupportLayerPtrs& ts_layers = m_object->support_layers(); SupportLayerPtrs& ts_layers = m_object->support_layers();
// Orca: the vector owns its layers, so the dropped ones are deleted before they are erased. auto iter = std::remove_if(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return ts_layer->height < EPSILON; });
// std::stable_partition (unlike std::remove_if) leaves exactly the dropped layers in the tail.
// Only heights below EPSILON are dropped; written as !(height < EPSILON), a NaN height is kept.
auto iter = std::stable_partition(ts_layers.begin(), ts_layers.end(), [](SupportLayer* ts_layer) { return !(ts_layer->height < EPSILON); });
for (auto it = iter; it != ts_layers.end(); ++it)
delete *it;
ts_layers.erase(iter, ts_layers.end()); ts_layers.erase(iter, ts_layers.end());
for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) { for (int layer_nr = 0; layer_nr < ts_layers.size(); layer_nr++) {
ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr; ts_layers[layer_nr]->upper_layer = layer_nr != ts_layers.size() - 1 ? ts_layers[layer_nr + 1] : nullptr;
@@ -3054,7 +3071,7 @@ void TreeSupport::drop_nodes()
//Insert a completely new node and let both original nodes fade. //Insert a completely new node and let both original nodes fade.
Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes. Point next_position = (node.position + neighbours[0]) / 2; //Average position of the two nodes.
coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next); coordf_t next_radius = calc_radius(node.dist_mm_to_top+height_next);
const ExPolygons& avoid_layer = get_avoidance(next_radius, obj_layer_nr_next); auto avoid_layer = get_avoidance(next_radius, obj_layer_nr_next);
if (group_index == 0) if (group_index == 0)
{ {
//Avoid collisions. //Avoid collisions.
@@ -3258,7 +3275,7 @@ void TreeSupport::drop_nodes()
} }
#endif #endif
coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next); coordf_t next_radius = calc_radius(node.dist_mm_to_top + height_next);
const ExPolygons& avoidance_next = get_avoidance(next_radius, obj_layer_nr_next); auto avoidance_next = get_avoidance(next_radius, obj_layer_nr_next);
Point to_outside = projection_onto(avoidance_next, node.position); Point to_outside = projection_onto(avoidance_next, node.position);
Point direction_to_outer = to_outside - node.position; Point direction_to_outer = to_outside - node.position;
@@ -3320,7 +3337,7 @@ void TreeSupport::drop_nodes()
p_node->to_buildplate = false; p_node->to_buildplate = false;
return; // from parallel_for_each lambda return; // from parallel_for_each lambda
} }
const ExPolygons& next_collision = get_collision(0, obj_layer_nr_next); auto next_collision = get_collision(0, obj_layer_nr_next);
const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex); const bool to_buildplate = !is_inside_ex(m_ts_data->m_layer_outlines[obj_layer_nr_next], next_layer_vertex);
// don't increase radius if next node will collide partially with the object (STUDIO-7883) // don't increase radius if next node will collide partially with the object (STUDIO-7883)
to_outside = projection_onto(next_collision, next_layer_vertex); to_outside = projection_onto(next_collision, next_layer_vertex);
+2 -2
View File
@@ -536,9 +536,9 @@ private:
coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true); coordf_t calc_branch_radius(coordf_t base_radius, coordf_t mm_to_top, double diameter_angle_scale_factor, bool use_min_distance=true);
coordf_t calc_radius(coordf_t mm_to_top); coordf_t calc_radius(coordf_t mm_to_top);
coordf_t get_radius(const SupportNode* node); coordf_t get_radius(const SupportNode* node);
const ExPolygons& get_avoidance(coordf_t radius, size_t obj_layer_nr); ExPolygons get_avoidance(coordf_t radius, size_t obj_layer_nr);
// layer's expolygon expanded by radius+m_xy_distance // layer's expolygon expanded by radius+m_xy_distance
const ExPolygons& get_collision(coordf_t radius, size_t layer_nr); ExPolygons get_collision(coordf_t radius, size_t layer_nr);
// get Polygons instead of ExPolygons // get Polygons instead of ExPolygons
Polygons get_collision_polys(coordf_t radius, size_t layer_nr); Polygons get_collision_polys(coordf_t radius, size_t layer_nr);
+25 -666
View File
@@ -19,7 +19,6 @@
#include <mutex> #include <mutex>
#include <numeric> #include <numeric>
#include <optional> #include <optional>
#include "nlohmann/json.hpp"
#include <queue> #include <queue>
#include <ratio> #include <ratio>
#include <string> #include <string>
@@ -158,33 +157,12 @@ struct SmoothedTextureCache
{ {
std::weak_ptr<const std::vector<unsigned char>> source; std::weak_ptr<const std::vector<unsigned char>> source;
float smoothing = 0.f; float smoothing = 0.f;
// Part of the key, not of the result: smoothing is applied *after* the panel's adjustments, so a
// blur computed over one set of adjustments is not the blur another set would give.
TextureAdjustments adjust;
DecodedHeightTexture texture; DecodedHeightTexture texture;
}; };
std::mutex mutex; std::mutex mutex;
std::unordered_map<const void *, Entry> entries; std::unordered_map<const void *, Entry> entries;
}; };
SmoothedTextureCache g_smoothed_texture_cache; SmoothedTextureCache g_smoothed_texture_cache;
// The adjusted copy, cached between the raw decode and the smoothed one. Needed for the same reason the
// smoothed cache is: while a slider in the image-adjustment panel moves, the thumbnail, the shaded
// preview's height and colour textures, the UV editor backdrop and the displacement preview job all ask
// for the same (image, adjustments) pair within one frame, and each of them running a full-size resample
// of its own is what would make the panel unusable on a large texture.
struct AdjustedTextureCache
{
struct Entry
{
std::weak_ptr<const std::vector<unsigned char>> source;
TextureAdjustments adjust;
DecodedHeightTexture texture;
};
std::mutex mutex;
std::unordered_map<const void *, Entry> entries;
};
AdjustedTextureCache g_adjusted_texture_cache;
} // namespace } // namespace
namespace { namespace {
@@ -360,34 +338,6 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result }; entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), result };
} }
// The image-adjustment panel's live edits, on top of the decode and before the smoothing. Before,
// because `preview_adjust` is an edit to the image while `smoothing` is a live low-pass over whatever
// the image is - the same order the panel presents them in, and the same order a commit bakes in.
//
// Nothing applies here once the panel has closed: it resets preview_adjust to identity, and identity
// is a no-op that costs one comparison.
if (!layer.preview_adjust.identity()) {
bool have_adjusted = false;
{
std::lock_guard<std::mutex> lock(g_adjusted_texture_cache.mutex);
auto it = g_adjusted_texture_cache.entries.find(key);
if (it != g_adjusted_texture_cache.entries.end() && it->second.adjust == layer.preview_adjust &&
it->second.source.lock() == layer.image_data) {
result = it->second.texture;
have_adjusted = true;
}
}
if (!have_adjusted) {
result = apply_texture_adjustments(result, layer.preview_adjust);
std::lock_guard<std::mutex> lock(g_adjusted_texture_cache.mutex);
auto &entries = g_adjusted_texture_cache.entries;
for (auto it = entries.begin(); it != entries.end();)
it = it->second.source.expired() ? entries.erase(it) : std::next(it);
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data),
layer.preview_adjust, result };
}
}
// Smoothing radius scales with the texture so the same slider value blurs the same *fraction* of // Smoothing radius scales with the texture so the same slider value blurs the same *fraction* of
// the image whatever resolution it came in at, and stays continuous in the slider - see // the image whatever resolution it came in at, and stays continuous in the slider - see
// smooth_height_pixels(). The cap is a cost limit, not part of the mapping: the blur is // smooth_height_pixels(). The cap is a cost limit, not part of the mapping: the blur is
@@ -398,7 +348,7 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
std::lock_guard<std::mutex> lock(g_smoothed_texture_cache.mutex); std::lock_guard<std::mutex> lock(g_smoothed_texture_cache.mutex);
auto it = g_smoothed_texture_cache.entries.find(key); auto it = g_smoothed_texture_cache.entries.find(key);
if (it != g_smoothed_texture_cache.entries.end() && it->second.smoothing == layer.smoothing && if (it != g_smoothed_texture_cache.entries.end() && it->second.smoothing == layer.smoothing &&
it->second.adjust == layer.preview_adjust && it->second.source.lock() == layer.image_data) it->second.source.lock() == layer.image_data)
return it->second.texture; return it->second.texture;
} }
const float span = 0.05f * float(std::min(result.width, result.height)); const float span = 0.05f * float(std::min(result.width, result.height));
@@ -422,70 +372,12 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
auto &entries = g_smoothed_texture_cache.entries; auto &entries = g_smoothed_texture_cache.entries;
for (auto it = entries.begin(); it != entries.end();) for (auto it = entries.begin(); it != entries.end();)
it = it->second.source.expired() ? entries.erase(it) : std::next(it); it = it->second.source.expired() ? entries.erase(it) : std::next(it);
entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), layer.smoothing, entries[key] = { std::weak_ptr<const std::vector<unsigned char>>(layer.image_data), layer.smoothing, result };
layer.preview_adjust, result };
} }
return result; return result;
} }
namespace { namespace {
// Fraction of the short side that TextureAdjustments::blur == 1 blurs over, and the cost cap on it.
// Deliberately the same pair decode_height_texture() uses for the Smoothing slider - see
// adjustment_blur_radius().
constexpr float SOURCE_BLUR_SPAN = 0.05f;
constexpr float SOURCE_BLUR_MAX_PX = 48.f;
// Radius of the unsharp mask's blurred reference, in texels. Small and only mildly resolution-dependent:
// a sharpen is meant to act on the finest detail the image holds, so its radius tracks the texel grid
// rather than the picture's size. 1024x1024 gives ~2 texels, the usual photo-editor default.
float unsharp_radius(int width, int height)
{
return std::max(1.f, 0.002f * float(std::min(width, height)));
}
// Brightness as a *lift* toward white (or toward black) rather than an additive offset: positive b maps
// v to v + b*(1 - v), negative to v*(1 + b). Both reach the endpoint exactly at |b| == 1 while staying
// monotonic and non-clipping in between, which an additive shift does not - it drives a whole region to
// a flat 255 and, because this image is also the height map, that region bakes as flat relief rather
// than as bright relief. Keeping the curve injective keeps the detail in it.
float apply_brightness(float v, float b)
{
return b >= 0.f ? v + b * (1.f - v) : v * (1.f + b);
}
// Contrast about the 0.5 pivot. Gain is 1/(1-c) going up (c == 1 would be an infinite-slope threshold, so
// the caller's clamp keeps it finite) and 1+c going down, reaching a flat mid-grey at c == -1.
float apply_contrast(float v, float c)
{
const float gain = c >= 0.f ? 1.f / std::max(1e-3f, 1.f - c) : 1.f + c;
return (v - 0.5f) * gain + 0.5f;
}
// Blur then sharpen one 8-bit channel in place. Shared by the grayscale path and by each of a colour
// image's three channels, so every channel gets the identical spatial treatment and a colour image's
// hue cannot shift just because its channels were filtered differently.
void adjust_channel_spatial(std::vector<uint8_t> &channel, int width, int height, float blur, float sharpness)
{
if (blur > 0.f)
smooth_height_pixels(channel, width, height, adjustment_blur_radius(blur, width, height));
if (sharpness <= 0.f)
return;
// Unsharp mask: push each texel away from its local average by `amount` times the difference. The
// blurred reference is a copy, so the sharpen reads the un-sharpened neighbourhood everywhere rather
// than its own partly-written output.
std::vector<uint8_t> base = channel;
smooth_height_pixels(base, width, height, unsharp_radius(width, height));
const float amount = 1.5f * std::clamp(sharpness, 0.f, 1.f);
tbb::parallel_for(tbb::blocked_range<size_t>(0, channel.size()), [&](const tbb::blocked_range<size_t> &r) {
for (size_t i = r.begin(); i < r.end(); ++i) {
const float v = float(channel[i]) + amount * (float(channel[i]) - float(base[i]));
channel[i] = uint8_t(std::lround(std::clamp(v, 0.f, 255.f)));
}
});
}
struct TextureDetailCache struct TextureDetailCache
{ {
std::mutex mutex; std::mutex mutex;
@@ -494,150 +386,6 @@ struct TextureDetailCache
TextureDetailCache g_texture_detail_cache; TextureDetailCache g_texture_detail_cache;
} // namespace } // namespace
float adjustment_blur_radius(float blur, int width, int height)
{
if (blur <= 0.f || width <= 0 || height <= 0)
return 0.f;
const float span = SOURCE_BLUR_SPAN * float(std::min(width, height));
return std::clamp(blur, 0.f, 1.f) * std::min(span, SOURCE_BLUR_MAX_PX);
}
DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, int max_px)
{
if (src.empty() || max_px <= 0 || (src.width <= max_px && src.height <= max_px))
return src;
const int scale = std::max(1, (std::max(src.width, src.height) + max_px - 1) / max_px);
const int w = std::max(1, src.width / scale);
const int h = std::max(1, src.height / scale);
const bool color = src.has_color();
DecodedHeightTexture out;
out.width = w;
out.height = h;
out.pixels.resize(size_t(w) * size_t(h));
if (color)
out.rgb.resize(size_t(w) * size_t(h) * 3);
tbb::parallel_for(tbb::blocked_range<int>(0, h), [&](const tbb::blocked_range<int> &r) {
for (int y = r.begin(); y < r.end(); ++y)
for (int x = 0; x < w; ++x) {
// The source block this destination texel covers. The max() keeps the block non-empty
// when rounding would otherwise collapse it.
const int x0 = x * src.width / w, x1 = std::max(x0 + 1, (x + 1) * src.width / w);
const int y0 = y * src.height / h, y1 = std::max(y0 + 1, (y + 1) * src.height / h);
uint32_t sum_h = 0, sum_c[3] = { 0, 0, 0 }, n = 0;
for (int sy = y0; sy < y1 && sy < src.height; ++sy)
for (int sx = x0; sx < x1 && sx < src.width; ++sx, ++n) {
const size_t si = size_t(sy) * size_t(src.width) + size_t(sx);
sum_h += src.pixels[si];
if (color)
for (int c = 0; c < 3; ++c)
sum_c[c] += src.rgb[si * 3 + size_t(c)];
}
const size_t di = size_t(y) * size_t(w) + size_t(x);
out.pixels[di] = n > 0 ? uint8_t(sum_h / n) : 0;
if (color)
for (int c = 0; c < 3; ++c)
out.rgb[di * 3 + size_t(c)] = n > 0 ? uint8_t(sum_c[c] / n) : 0;
}
});
return out;
}
DecodedHeightTexture apply_texture_adjustments(const DecodedHeightTexture &src, const TextureAdjustments &adj)
{
DecodedHeightTexture out = src;
if (src.empty() || adj.identity())
return out;
const int w = out.width;
const int h = out.height;
const size_t n = size_t(w) * size_t(h);
const float brightness = std::clamp(adj.brightness, -1.f, 1.f);
// Short of 1, so apply_contrast()'s gain stays finite; at this value it is already a 1000x slope,
// i.e. a threshold in all but name.
const float contrast = std::clamp(adj.contrast, -1.f, 0.999f);
const float blur = std::clamp(adj.blur, 0.f, 1.f);
const float sharpness = std::clamp(adj.sharpness, 0.f, 1.f);
// The tonal pass: a 256-entry lookup table, because brightness and contrast are the same scalar
// curve for every texel and every channel. Building it once beats evaluating two curves a few
// million times, and it is what keeps this pass off the panel's critical path.
std::array<uint8_t, 256> tone{};
for (int i = 0; i < 256; ++i) {
float v = float(i) / 255.f;
v = apply_brightness(v, brightness);
v = apply_contrast(v, contrast);
tone[size_t(i)] = uint8_t(std::lround(std::clamp(v, 0.f, 1.f) * 255.f));
}
if (!out.has_color()) {
// Grayscale: one channel, and hue has nothing to turn.
tbb::parallel_for(tbb::blocked_range<size_t>(0, n), [&](const tbb::blocked_range<size_t> &r) {
for (size_t i = r.begin(); i < r.end(); ++i)
out.pixels[i] = tone[out.pixels[i]];
});
adjust_channel_spatial(out.pixels, w, h, blur, sharpness);
return out;
}
// Colour: tone each channel through the same table, then rotate the hue about the luminance axis.
//
// The rotation is feColorMatrix's hueRotate matrix (SVG filters), which holds Rec.709 luma fixed.
// The height below is taken with decode_height_texture()'s Rec.601 coefficients instead, so a hue
// turn is not perfectly height-neutral - a few levels at full saturation. Reconciling the two would
// mean a different rotation than every other tool's, which is the worse trade: the user is watching
// the colour, and the height follows the colour by construction either way.
const float theta = adj.hue_deg * float(M_PI) / 180.f;
const float c = std::cos(theta);
const float s = std::sin(theta);
const float m[9] = {
0.213f + c * 0.787f - s * 0.213f, 0.715f - c * 0.715f - s * 0.715f, 0.072f - c * 0.072f + s * 0.928f,
0.213f - c * 0.213f + s * 0.143f, 0.715f + c * 0.285f + s * 0.140f, 0.072f - c * 0.072f - s * 0.283f,
0.213f - c * 0.213f - s * 0.787f, 0.715f - c * 0.715f + s * 0.715f, 0.072f + c * 0.928f + s * 0.072f,
};
const bool turn_hue = adj.hue_deg != 0.f;
tbb::parallel_for(tbb::blocked_range<size_t>(0, n), [&](const tbb::blocked_range<size_t> &r) {
for (size_t i = r.begin(); i < r.end(); ++i) {
const float rr = float(tone[out.rgb[i * 3]]);
const float gg = float(tone[out.rgb[i * 3 + 1]]);
const float bb = float(tone[out.rgb[i * 3 + 2]]);
const float nr = turn_hue ? m[0] * rr + m[1] * gg + m[2] * bb : rr;
const float ng = turn_hue ? m[3] * rr + m[4] * gg + m[5] * bb : gg;
const float nb = turn_hue ? m[6] * rr + m[7] * gg + m[8] * bb : bb;
out.rgb[i * 3] = uint8_t(std::lround(std::clamp(nr, 0.f, 255.f)));
out.rgb[i * 3 + 1] = uint8_t(std::lround(std::clamp(ng, 0.f, 255.f)));
out.rgb[i * 3 + 2] = uint8_t(std::lround(std::clamp(nb, 0.f, 255.f)));
}
});
// The spatial pass wants one channel at a time (smooth_height_pixels() is a single-channel blur, and
// reusing it is what keeps this consistent with Smoothing), so de-interleave, filter, re-interleave.
if (blur > 0.f || sharpness > 0.f) {
std::vector<uint8_t> channel(n);
for (int ch = 0; ch < 3; ++ch) {
for (size_t i = 0; i < n; ++i)
channel[i] = out.rgb[i * 3 + size_t(ch)];
adjust_channel_spatial(channel, w, h, blur, sharpness);
for (size_t i = 0; i < n; ++i)
out.rgb[i * 3 + size_t(ch)] = channel[i];
}
}
// Height is the luminance of the colour that came out, not the toned luminance of the colour that
// went in: the two differ once hue rotation or a per-channel clamp has moved the channels around,
// and a height that disagrees with the colour it is stored next to is the bug this avoids.
tbb::parallel_for(tbb::blocked_range<size_t>(0, n), [&](const tbb::blocked_range<size_t> &r) {
for (size_t i = r.begin(); i < r.end(); ++i)
out.pixels[i] = uint8_t(std::lround(0.299 * out.rgb[i * 3] + 0.587 * out.rgb[i * 3 + 1] +
0.114 * out.rgb[i * 3 + 2]));
});
return out;
}
TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer) TextureDetail analyze_texture_detail(const TextureDisplacementLayer &layer)
{ {
TextureDetail out; TextureDetail out;
@@ -743,57 +491,6 @@ V2Resolution recommend_v2_resolution(const indexed_triangle_set
return out; return out;
} }
namespace {
// Which side of a world plane a face is on, with an exactly zero component counted as the positive
// side. The tie never shows: a normal component of zero carries zero tri-planar blend weight (see
// triplanar_weights()), so the plane it orients contributes nothing at that point.
inline float box_face_sign(float n) { return n >= 0.f ? 1.f : -1.f; }
// The box projection's 2D basis for one of the three world planes, oriented the way a viewer
// standing outside the model sees that face.
//
// v runs *down* the image, because that is what v means to the sampler: v = 0 is the texture's first
// pixel row, i.e. its top (DecodedHeightTexture::sample(); the UV editor negates Y on screen for the
// same reason, see UVEditorCanvas::render()). So v = -z on the four upright planes and v = -y on the
// horizontal one, which is what puts the top of an image at the top of the model rather than
// upside down.
//
// u then has to depend on which *side* of the plane the face is on, or one of the two faces sharing
// a plane comes out left-right mirrored: with a single u per plane, a portrait image reads upright on
// +X and 180 degrees round on -X. Fixing that is the sign below, chosen so that u x v == -normal,
// i.e. (right, down, into the surface) is right-handed for a viewer outside:
//
// face u v face u v face u v
// +X +y -z +Y -x -z +Z +x -y
// -X -y -z -Y +x -z -Z -x -y
//
// The sign flips where its own normal component crosses zero, and the blend weight of the plane it
// belongs to is exactly zero there, so the blended tri-planar sum stays continuous across the flip -
// it is not a return of the hard axis switch described on TextureProjectionMethod::Triplanar.
inline Vec2f box_plane_yz(const Vec3f &p, float nx) { return Vec2f(box_face_sign(nx) * p.y(), -p.z()); }
inline Vec2f box_plane_xz(const Vec3f &p, float ny) { return Vec2f(-box_face_sign(ny) * p.x(), -p.z()); }
inline Vec2f box_plane_xy(const Vec3f &p, float nz) { return Vec2f(box_face_sign(nz) * p.x(), -p.y()); }
} // namespace
void box_projection_axes(const Vec3f &normal, Vec3f &u_axis, Vec3f &v_axis)
{
// The same table as box_plane_yz()/box_plane_xz()/box_plane_xy(), handed out as a pair of world
// directions for the plane the normal is closest to. Two shapes of one table, because the bake's
// hot path wants the closed form and the GUI wants the axes; the test suite checks them against
// each other so they cannot drift apart.
const Vec3f n = normal.cwiseAbs();
if (n.x() >= n.y() && n.x() >= n.z()) {
u_axis = box_face_sign(normal.x()) * Vec3f::UnitY();
v_axis = -Vec3f::UnitZ();
} else if (n.y() >= n.x() && n.y() >= n.z()) {
u_axis = -box_face_sign(normal.y()) * Vec3f::UnitX();
v_axis = -Vec3f::UnitZ();
} else {
u_axis = box_face_sign(normal.z()) * Vec3f::UnitX();
v_axis = -Vec3f::UnitY();
}
}
Vec2f project_planar(const Vec3f &position, const Vec3f &normal) Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
{ {
// Planar-project onto the two axes orthogonal to the dominant component of `normal`. Called // Planar-project onto the two axes orthogonal to the dominant component of `normal`. Called
@@ -804,26 +501,23 @@ Vec2f project_planar(const Vec3f &position, const Vec3f &normal)
// other face in the same patch - exactly the bug an earlier version of this feature had). // other face in the same patch - exactly the bug an earlier version of this feature had).
const Vec3f n = normal.cwiseAbs(); const Vec3f n = normal.cwiseAbs();
if (n.x() >= n.y() && n.x() >= n.z()) if (n.x() >= n.y() && n.x() >= n.z())
return box_plane_yz(position, normal.x()); return Vec2f(position.y(), position.z());
if (n.y() >= n.x() && n.y() >= n.z()) if (n.y() >= n.x() && n.y() >= n.z())
return box_plane_xz(position, normal.y()); return Vec2f(position.x(), position.z());
return box_plane_xy(position, normal.z()); return Vec2f(position.x(), position.y());
} }
namespace { namespace {
// Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at // Wrapped around patch_axis, centered at patch_center. u is the arc length (mm) around the axis at
// this point's own radius, v is the distance along the axis, measured *against* it - a reasonable // this point's own radius, v is the signed distance along the axis - a reasonable approximation
// approximation for roughly cylindrical selections, not an exact fit for arbitrary geometry. // for roughly cylindrical selections, not an exact fit for arbitrary geometry.
Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis) Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, const Vec3f &patch_axis)
{ {
Vec3f up = patch_axis; Vec3f up = patch_axis;
up = (up.norm() > 1e-8f) ? Vec3f(up.normalized()) : Vec3f::UnitZ(); up = (up.norm() > 1e-8f) ? Vec3f(up.normalized()) : Vec3f::UnitZ();
const Vec3f arbitrary = (std::abs(up.dot(Vec3f::UnitZ())) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX(); const Vec3f arbitrary = (std::abs(up.dot(Vec3f::UnitZ())) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
const Vec3f right = Vec3f(up.cross(arbitrary).normalized()); const Vec3f right = Vec3f(up.cross(arbitrary).normalized());
// up x right, not right x up: that makes (right, fwd, up) right-handed, so the angle below grows const Vec3f fwd = Vec3f(right.cross(up).normalized());
// anticlockwise about the axis - which, seen from outside the cylinder, is to the viewer's right.
// The other way round the texture wraps on mirrored, as if seen through the model from behind.
const Vec3f fwd = Vec3f(up.cross(right).normalized());
const Vec3f rel = position - patch_center; const Vec3f rel = position - patch_center;
const float along_axis = rel.dot(up); const float along_axis = rel.dot(up);
@@ -832,9 +526,7 @@ Vec2f project_cylindrical(const Vec3f &position, const Vec3f &patch_center, cons
const float radius = std::sqrt(x * x + y * y); const float radius = std::sqrt(x * x + y * y);
const float angle = std::atan2(y, x); const float angle = std::atan2(y, x);
// Negated, like every other projection's v: v = 0 is the texture's top row, so v has to grow return Vec2f(angle * radius, along_axis);
// *down* the axis for the top of the image to sit at the top of the model.
return Vec2f(angle * radius, -along_axis);
} }
// Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the // Longitude/latitude around patch_center. u/v are scaled by this point's own distance from the
@@ -850,9 +542,7 @@ Vec2f project_spherical(const Vec3f &position, const Vec3f &patch_center)
const Vec3f dir = rel / radius; const Vec3f dir = rel / radius;
const float longitude = std::atan2(dir.y(), dir.x()); const float longitude = std::atan2(dir.y(), dir.x());
const float latitude = std::asin(std::clamp(dir.z(), -1.f, 1.f)); const float latitude = std::asin(std::clamp(dir.z(), -1.f, 1.f));
// Longitude already grows to the right as seen from outside; latitude grows toward the north return Vec2f(longitude, latitude) * radius;
// pole, so it is negated to put the image's top row (v = 0) at the top of the model.
return Vec2f(longitude, -latitude) * radius;
} }
// CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but // CGAL's LSCM parameterizer expects a clean mesh with no isolated (unreferenced) vertices - but
@@ -1147,26 +837,9 @@ std::vector<int> segment_into_charts(const indexed_triangle_set &mesh, const std
// solve at all, and why this also serves as the fallback for a chart LSCM cannot handle. // solve at all, and why this also serves as the fallback for a chart LSCM cannot handle.
std::vector<Vec2f> project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal) std::vector<Vec2f> project_to_tangent_plane(const indexed_triangle_set &chart, const Vec3f &normal)
{ {
// Oriented like the box projection (see box_projection_axes()), so a chart reads the right way up const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
// and unmirrored instead of being flattened to whichever basis a cross product happened to give. const Vec3f u = Vec3f(seed.cross(normal).normalized());
// For a chart whose normal *is* a world axis - a cube face, which is the common case once the const Vec3f v = Vec3f(normal.cross(u).normalized());
// seam angle has cut the patch along its sharp edges - this is exactly the box basis, so such a
// face unwraps to the same orientation Triplanar would have given it.
Vec3f u, v;
box_projection_axes(normal, u, v);
// The box axes lie in the tangent plane already when the normal is a world axis; for any other
// normal, u is squared up against it and v follows. u x v == -normal keeps the chart the way
// round a viewer outside the model sees it (a plain normal x u would mirror it).
u = u - normal * normal.dot(u);
if (u.norm() <= 1e-6f) {
// Out of reach for a unit normal - the box basis puts u perpendicular to the normal's own
// dominant axis, so the two can never be within 45 degrees of each other - but a caller
// handing over an unnormalized normal should still get a usable basis rather than a NaN.
const Vec3f seed = (std::abs(normal.z()) < 0.9f) ? Vec3f::UnitZ() : Vec3f::UnitX();
u = seed.cross(normal);
}
u = Vec3f(u.normalized());
v = Vec3f(u.cross(normal).normalized());
std::vector<Vec2f> uvs(chart.vertices.size()); std::vector<Vec2f> uvs(chart.vertices.size());
for (size_t i = 0; i < chart.vertices.size(); ++i) for (size_t i = 0; i < chart.vertices.size(); ++i)
@@ -1897,61 +1570,6 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
return islands; 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) void average_island_scales(std::vector<TextureIsland> &islands)
{ {
if (islands.empty()) if (islands.empty())
@@ -2196,10 +1814,8 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
return texture.sample(uv, layer.tile_enabled, layer.tile_method); return texture.sample(uv, layer.tile_enabled, layer.tile_method);
} }
// Flat projection onto the captured projector plane. Single-valued per point, so unlike // Flat projection onto the captured projector plane. Single-valued per point, so unlike
// blended triplanar it is one sample, and it is what "project from view" places. The // blended triplanar it is one sample, and it is what "project from view" places.
// projector's up axis is negated because v runs *down* the image (v = 0 is its first pixel return sample_at(Vec2f(position.dot(layer.view_project_right), position.dot(layer.view_project_up)));
// row), the same reason the projective branch above measures window y downward.
return sample_at(Vec2f(position.dot(layer.view_project_right), -position.dot(layer.view_project_up)));
case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar case TextureProjectionMethod::LSCM: // no usable unwrap for this patch - fall back to Triplanar
case TextureProjectionMethod::Triplanar: case TextureProjectionMethod::Triplanar:
default: break; default: break;
@@ -2214,15 +1830,14 @@ float sample_layer_height(const DecodedHeightTexture &texture, const TextureDisp
const float w_sum = w.x() + w.y() + w.z(); const float w_sum = w.x() + w.y() + w.z();
if (w_sum < 1e-8f) if (w_sum < 1e-8f)
// Degenerate normal: no axis is meaningfully dominant, so no blend is meaningful either. // Degenerate normal: no axis is meaningfully dominant, so no blend is meaningful either.
return sample_at(box_plane_xy(position, normal.z())); return sample_at(Vec2f(position.x(), position.y()));
w /= w_sum; w /= w_sum;
// Each plane drops the axis it is named for and is oriented for the side of that plane the normal // Each plane drops the axis it is named for, matching project_planar()'s own convention (which
// points to, matching project_planar()'s own convention (which the GUI's on-canvas placement // the GUI's on-canvas placement gizmo also relies on).
// gizmo also relies on). See box_plane_yz() for why the orientation is not simply (y, z). return w.x() * sample_at(Vec2f(position.y(), position.z())) +
return w.x() * sample_at(box_plane_yz(position, normal.x())) + w.y() * sample_at(Vec2f(position.x(), position.z())) +
w.y() * sample_at(box_plane_xz(position, normal.y())) + w.z() * sample_at(Vec2f(position.x(), position.y()));
w.z() * sample_at(box_plane_xy(position, normal.z()));
} }
bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer, bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDisplacementLayer &layer,
@@ -2284,8 +1899,7 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
out = texture.sample_color(uv, layer.tile_enabled, layer.tile_method); out = texture.sample_color(uv, layer.tile_enabled, layer.tile_method);
return true; return true;
} else { } else {
// Negated up, exactly as sample_layer_height() does it. const Vec2f p(position.dot(layer.view_project_right), position.dot(layer.view_project_up));
const Vec2f p(position.dot(layer.view_project_right), -position.dot(layer.view_project_up));
if (!covered(p)) if (!covered(p))
return false; return false;
out = sample_at(p); out = sample_at(p);
@@ -2301,7 +1915,7 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
Vec3f w = triplanar_weights(normal); Vec3f w = triplanar_weights(normal);
const float w_sum = w.x() + w.y() + w.z(); const float w_sum = w.x() + w.y() + w.z();
if (w_sum < 1e-8f) { if (w_sum < 1e-8f) {
const Vec2f p = box_plane_xy(position, normal.z()); const Vec2f p(position.x(), position.y());
if (!covered(p)) if (!covered(p))
return false; return false;
out = sample_at(p); out = sample_at(p);
@@ -2312,8 +1926,8 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
// A blend of three planes is only "not covered" where *every* contributing plane is outside the // A blend of three planes is only "not covered" where *every* contributing plane is outside the
// placement; where some are, the covered ones are renormalised so the colour does not fade toward // placement; where some are, the covered ones are renormalised so the colour does not fade toward
// black at the edge of an untiled tri-planar layer. // black at the edge of an untiled tri-planar layer.
const std::array<Vec2f, 3> planes = { box_plane_yz(position, normal.x()), box_plane_xz(position, normal.y()), const std::array<Vec2f, 3> planes = { Vec2f(position.y(), position.z()), Vec2f(position.x(), position.z()),
box_plane_xy(position, normal.z()) }; Vec2f(position.x(), position.y()) };
Vec3f acc = Vec3f::Zero(); Vec3f acc = Vec3f::Zero();
float acc_w = 0.f; float acc_w = 0.f;
for (int i = 0; i < 3; ++i) for (int i = 0; i < 3; ++i)
@@ -5552,260 +5166,5 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
return out; 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 } // namespace Slic3r
+5 -144
View File
@@ -154,38 +154,6 @@ struct TextureIsland
// chart to its true mm area, so a freshly unwrapped patch has uniform texel density to begin with. // chart to its true mm area, so a freshly unwrapped patch has uniform texel density to begin with.
void average_island_scales(std::vector<TextureIsland> &islands); void average_island_scales(std::vector<TextureIsland> &islands);
// Photo-style adjustments to a layer's source image, as the gizmo's image-adjustment panel sets them.
//
// These are *not* layer parameters: nothing here is stored on TextureDisplacementLayer and nothing here is
// re-applied at bake time. The panel applies them once, to the image, and the result becomes the layer's
// image - so the adjusted pixels feed the colour and the displacement together, and a brightness change
// really does change the relief. That is the intended behaviour: one image, one source of truth, rather
// than a second set of knobs the bake would have to replay in the right order.
//
// Contrast with TextureDisplacementLayer::smoothing, which *is* a layer parameter applied on every decode:
// `blur` here is a destructive edit of the image, `smoothing` is a live low-pass over whatever the image is.
struct TextureAdjustments
{
float brightness = 0.f; // [-1, 1]; 0 unchanged, -1 black, +1 white
float contrast = 0.f; // [-1, 1]; 0 unchanged, -1 flat mid-grey, +1 hard threshold
float sharpness = 0.f; // [0, 1]; 0 unchanged. Unsharp-mask amount
float blur = 0.f; // [0, 1]; 0 unchanged. Fraction of SOURCE_BLUR_SPAN, as `smoothing` is
float hue_deg = 0.f; // [-180, 180]; 0 unchanged. No effect on a grayscale image - grey has no hue
bool identity() const
{
return brightness == 0.f && contrast == 0.f && sharpness == 0.f && blur == 0.f && hue_deg == 0.f;
}
// Exact equality, which is what is wanted: these come straight off sliders, and the question asked of
// them is "is this still the value the preview on screen was computed from", not "are these close".
bool operator==(const TextureAdjustments &o) const
{
return brightness == o.brightness && contrast == o.contrast && sharpness == o.sharpness &&
blur == o.blur && hue_deg == o.hue_deg;
}
bool operator!=(const TextureAdjustments &o) const { return !(*this == o); }
};
// One texture asset plus its projection/displacement parameters. Several layers may be painted // One texture asset plus its projection/displacement parameters. Several layers may be painted
// onto overlapping areas of the same volume: their displacements are combined per vertex, in slot // onto overlapping areas of the same volume: their displacements are combined per vertex, in slot
// order, each layer folding into the total via its own TextureBlendMode (see // order, each layer folding into the total via its own TextureBlendMode (see
@@ -309,18 +277,6 @@ struct TextureDisplacementLayer
// - zero or positive: a mesh vertex, every unwrapped copy of it (how edits were stored before). // - zero or positive: a mesh vertex, every unwrapped copy of it (how edits were stored before).
std::vector<std::pair<int, Vec2f>> lscm_uv_overrides; std::vector<std::pair<int, Vec2f>> lscm_uv_overrides;
// Live, unsaved image adjustments from the gizmo's image-adjustment panel, applied by
// decode_height_texture() on top of the decoded image - so while the panel is open the relief, the
// colour, the thumbnails and the bake all see the adjusted image, and moving a slider really does
// change the displacement. See TextureAdjustments.
//
// Deliberately *not* serialized, by either save()/load() here or the .3mf: it is the state of an open
// editor, not a property of the layer. The panel resets it when it closes, and "Save as copy" resolves
// it by writing a new image and pointing the layer at that - which is the only way an adjustment
// becomes permanent. A project saved mid-adjustment therefore stores the unadjusted image, which is
// the honest answer: nothing was committed.
TextureAdjustments preview_adjust;
// How this layer folds into the displacement accumulated by the layers below it. Ignored for // How this layer folds into the displacement accumulated by the layers below it. Ignored for
// the lowest-slot painted layer, which has nothing beneath it to combine with (the GUI shows // the lowest-slot painted layer, which has nothing beneath it to combine with (the GUI shows
// it as the "Base" layer and hides the control). // it as the "Base" layer and hides the control).
@@ -441,41 +397,14 @@ struct TextureDisplacementOptions
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide. // finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
int color_despeckle = 2; 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) template<class Archive> void serialize(Archive &ar)
{ {
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border, ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k, pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, v2_flip_edges, color_mix_enabled, color_despeckle, color_mix_count, v2_relocate, 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 // 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 // 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. // span (1 for a hard-edged image, 4 for a smooth one). Cached per image, like the decode.
@@ -548,36 +477,6 @@ struct DecodedHeightTexture
bool texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method, TexelTap &out) const; bool texel_tap(const Vec2f &uv, bool tile_enabled, TextureTileMethod tile_method, TexelTap &out) const;
}; };
// Applies `adj` to `src` and returns the result; `src` is left alone. An empty `src`, or an identity
// `adj`, gives back a copy.
//
// Order is tonal, then spatial: brightness, contrast and hue per texel first, then blur, then sharpness.
// Sharpness last is what lets the two spatial knobs be used together - blur to kill speckle, then
// sharpness to put the real edges back - instead of the blur simply undoing the sharpen.
//
// Grayscale in, grayscale out (`rgb` stays empty): promoting a grey image to RGB would hand the layer's
// colour feature a row of identical greys to "colour" the model with, and triple the stored PNG for it.
// For a colour image the height is recomputed as the luminance of the *adjusted* colour, with
// decode_height_texture()'s own coefficients, so the relief and the colour can never drift apart.
DecodedHeightTexture apply_texture_adjustments(const DecodedHeightTexture &src, const TextureAdjustments &adj);
// Blur radius, in texels, that TextureAdjustments::blur == 1 means on a `min(width, height)`-sized image:
// the same 5%-of-the-short-side span the Smoothing slider uses, under the same 48-texel cost cap, so the
// two controls bite at the same rate and a user moving between them is not surprised.
float adjustment_blur_radius(float blur, int width, int height);
// Box-filters `src` down so neither side exceeds `max_px`, preserving aspect; never upscales, and
// returns a copy when it already fits. Colour and height are filtered the same way, and the height
// stays the height - it is not recomputed from the filtered colour, because a box filter is linear and
// luminance is linear in the channels, so the two agree already.
//
// This is what lets the adjustment panel's live preview cost depend on the size of the box it is drawn
// in rather than on the size of the image: adjusting a downscaled copy per slider frame, and the
// full-size image only once, on commit. A box filter (rather than a plain decimation) is the point -
// these textures are high-frequency by nature, and dropping texels would alias the preview into
// showing detail the adjustment is not actually producing.
DecodedHeightTexture downscale_height_texture(const DecodedHeightTexture &src, int max_px);
// Decode a layer's raw image bytes into sampleable height data, plus colour when the source has any. // Decode a layer's raw image bytes into sampleable height data, plus colour when the source has any.
// Both 8-bit grayscale PNGs (the shipped library, and anything imported before colour was kept) and // Both 8-bit grayscale PNGs (the shipped library, and anything imported before colour was kept) and
// colour PNGs are accepted; for a colour source the height is its luminance, using the same // colour PNGs are accepted; for a colour source the height is its luminance, using the same
@@ -625,24 +524,11 @@ struct TextureColorSettings
bool empty() const { return palette.empty(); } bool empty() const { return palette.empty(); }
}; };
// The two world directions the box projection takes its u and v along on a face whose outward
// normal is `normal`: the plane is the one that normal is closest to, and the pair is oriented as a
// viewer standing outside the model sees it - u to their right, v *down* the image, which is the
// direction v runs in the texture (v = 0 is the image's first pixel row, see
// DecodedHeightTexture::sample()). So an image comes out the way it looks in a file manager rather
// than upside down, and it is not mirrored on the three faces facing the other way.
//
// Exposed because the GUI needs the same basis for the on-canvas "adjust texture placement" gizmo
// (its ring's zero angle and its offset handle's plane), and it must not be a second, drifting copy
// of the table.
void box_projection_axes(const Vec3f &normal, Vec3f &u_axis, Vec3f &v_axis);
// Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any // Raw dominant-axis planar projection of `position` (in mm, not yet scaled/rotated/offset by any
// layer), dropping the axis position that best aligns with `normal`. Equivalent to projecting onto // layer), dropping the axis position that best aligns with `normal`. Exposed on its own (rather
// box_projection_axes()' pair. Exposed on its own (rather than only inline inside // than only inline inside project_texture_displacement_uv()) so GUI code - the on-canvas
// project_texture_displacement_uv()) so GUI code - the on-canvas "adjust texture placement" gizmo - // "adjust texture placement" gizmo - can map a dragged 3D point into the exact same 2D space
// can map a dragged 3D point into the exact same 2D space tiling_scale/rotation_deg/offset operate // tiling_scale/rotation_deg/offset operate in, without duplicating the axis-selection logic.
// in, without duplicating the axis-selection logic.
Vec2f project_planar(const Vec3f &position, const Vec3f &normal); Vec2f project_planar(const Vec3f &position, const Vec3f &normal);
// Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate // Applies a layer's tiling_scale/rotation_deg/offset to an already-projected planar coordinate
@@ -692,31 +578,6 @@ bool sample_layer_color(const DecodedHeightTexture &texture, const TextureDispla
const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(), const Vec3f &patch_center = Vec3f::Zero(), const Vec3f &patch_axis = Vec3f::UnitZ(),
const Vec2f *lscm_uv = nullptr); 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 // 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 // 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 // projection axis. Used by the GUI to anchor the on-canvas "adjust texture placement" gizmo to
@@ -673,14 +673,14 @@ static bool repair_cluster_smooth(
[&]() { return cancelled(); }, &repair_error); [&]() { return cancelled(); }, &repair_error);
if (repaired) { if (repaired) {
if (cancelled()) return false; if (cancelled()) return false;
BOOST_LOG_TRIVIAL(info) << log_prefix << ": mesh repair finished."; BOOST_LOG_TRIVIAL(info) << log_prefix << ": Windows 3D mesh repair finished.";
if (!resample_face_colors(TriMesh(std::move(repaired_its)))) if (!resample_face_colors(TriMesh(std::move(repaired_its))))
return false; return false;
} else { } else {
BOOST_LOG_TRIVIAL(warning) << log_prefix << ": mesh repair failed: " << repair_error; BOOST_LOG_TRIVIAL(warning) << log_prefix << ": Windows 3D mesh repair failed: " << repair_error;
} }
} else { } else {
BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without repair."; BOOST_LOG_TRIVIAL(info) << log_prefix << ": importing mesh without Windows 3D repair.";
} }
} }
} }
+4 -1
View File
@@ -1,4 +1,5 @@
#pragma once #ifndef libslic3r_Timer_hpp_
#define libslic3r_Timer_hpp_
#include "TriangleMesh.hpp" #include "TriangleMesh.hpp"
@@ -9,3 +10,5 @@ public:
static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok); static TriangleMesh smooth_triangle_mesh(const TriangleMesh &mesh,bool& ok);
}; };
} // namespace Slic3r } // namespace Slic3r
#endif // libslic3r_Timer_hpp_
+1
View File
@@ -109,6 +109,7 @@ extern std::string format_memsize_MB(size_t n);
extern std::string format_memsize(size_t bytes, unsigned int decimals = 1); 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. // 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. // 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 std::string log_memory_info(bool ignore_loglevel = false);
extern void disable_multi_threading(); extern void disable_multi_threading();
// Returns the size of physical memory (RAM) in bytes. // Returns the size of physical memory (RAM) in bytes.
+3 -1
View File
@@ -1744,7 +1744,9 @@ std::string format_diameter_to_str(double diameter, int precision)
return oss.str(); return oss.str();
} }
// Returns platform-specific string to be used as log output. // 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).
std::string log_memory_info(bool ignore_loglevel) std::string log_memory_info(bool ignore_loglevel)
{ {
std::string out; std::string out;
+4 -6
View File
@@ -21,9 +21,7 @@ namespace libvgcode {
// the triangles that share it: the shader runs 8 times per segment instead of 24. // the triangles that share it: the shader runs 8 times per segment instead of 24.
static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 }; static constexpr const std::array<uint8_t, 8> VERTEX_DATA = { 0, 1, 2, 3, 4, 5, 6, 7 };
// 16-bit indices: GCN 1/2 GPUs have no 8-bit index support, and some of their drivers static constexpr const std::array<uint8_t, 24> INDEX_DATA = {
// emulate it wrongly in instanced draws.
static constexpr const std::array<uint16_t, 24> INDEX_DATA = {
0, 1, 2, // front spike 0, 1, 2, // front spike
0, 2, 3, // front spike 0, 2, 3, // front spike
0, 3, 4, // right/bottom body 0, 3, 4, // right/bottom body
@@ -39,7 +37,7 @@ void SegmentTemplate::init()
if (m_vao_id != 0) if (m_vao_id != 0)
return; return;
m_size_in_bytes_gpu += VERTEX_DATA.size() * sizeof(uint8_t) + INDEX_DATA.size() * sizeof(uint16_t); m_size_in_bytes_gpu += (VERTEX_DATA.size() + INDEX_DATA.size()) * sizeof(uint8_t);
int curr_vertex_array; int curr_vertex_array;
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
@@ -63,7 +61,7 @@ void SegmentTemplate::init()
// and restored together with the vao. // and restored together with the vao.
glsafe(glGenBuffers(1, &m_ibo_id)); glsafe(glGenBuffers(1, &m_ibo_id));
glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id)); glsafe(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, m_ibo_id));
glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint16_t), INDEX_DATA.data(), GL_STATIC_DRAW)); glsafe(glBufferData(GL_ELEMENT_ARRAY_BUFFER, INDEX_DATA.size() * sizeof(uint8_t), INDEX_DATA.data(), GL_STATIC_DRAW));
glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer)); glsafe(glBindBuffer(GL_ARRAY_BUFFER, curr_array_buffer));
glsafe(glBindVertexArray(curr_vertex_array)); glsafe(glBindVertexArray(curr_vertex_array));
@@ -96,7 +94,7 @@ void SegmentTemplate::render(size_t count)
glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array)); glsafe(glGetIntegerv(GL_VERTEX_ARRAY_BINDING, &curr_vertex_array));
glsafe(glBindVertexArray(m_vao_id)); glsafe(glBindVertexArray(m_vao_id));
glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_SHORT, glsafe(glDrawElementsInstanced(GL_TRIANGLES, static_cast<GLsizei>(INDEX_DATA.size()), GL_UNSIGNED_BYTE,
nullptr, static_cast<GLsizei>(count))); nullptr, static_cast<GLsizei>(count)));
glsafe(glBindVertexArray(curr_vertex_array)); glsafe(glBindVertexArray(curr_vertex_array));
} }
+3 -6
View File
@@ -927,17 +927,15 @@ void ViewerImpl::reset()
m_used_extruders.clear(); m_used_extruders.clear();
m_total_time = { 0.0f, 0.0f }; m_total_time = { 0.0f, 0.0f };
m_travels_time = { 0.0f, 0.0f }; m_travels_time = { 0.0f, 0.0f };
m_vertices.clear();
m_vertices_colors.clear();
// swap rather than clear: these are sized by the print, and a reset means the memory // swap rather than clear: these are sized by the print, and a reset means the memory
// should go back, not sit reserved until the next load // should go back, not sit reserved until the next load
std::vector<PathVertex>().swap(m_vertices);
std::vector<float>().swap(m_vertices_colors);
for (std::vector<float>& times : m_layer_start_times) for (std::vector<float>& times : m_layer_start_times)
std::vector<float>().swap(times); std::vector<float>().swap(times);
std::vector<uint32_t>().swap(m_layer_first_vertex); std::vector<uint32_t>().swap(m_layer_first_vertex);
m_layers_in_vertex_order = false; m_layers_in_vertex_order = false;
// BitSet::clear() only zeroes the bits, it keeps the blocks allocated; load() builds a new m_valid_lines_bitset.clear();
// bitset anyway and it is never read while m_vertices is empty
m_valid_lines_bitset = BitSet<>();
#if VGCODE_ENABLE_COG_AND_TOOL_MARKERS #if VGCODE_ENABLE_COG_AND_TOOL_MARKERS
m_cog_marker.reset(); m_cog_marker.reset();
#endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS #endif // VGCODE_ENABLE_COG_AND_TOOL_MARKERS
@@ -1836,7 +1834,6 @@ size_t ViewerImpl::get_used_cpu_memory() const
ret += sizeof(m_extrusion_roles_colors); ret += sizeof(m_extrusion_roles_colors);
ret += sizeof(m_options_colors); ret += sizeof(m_options_colors);
ret += STDVEC_MEMSIZE(m_vertices, PathVertex); ret += STDVEC_MEMSIZE(m_vertices, PathVertex);
ret += STDVEC_MEMSIZE(m_vertices_colors, float);
for (const std::vector<float>& times : m_layer_start_times) for (const std::vector<float>& times : m_layer_start_times)
ret += STDVEC_MEMSIZE(times, float); ret += STDVEC_MEMSIZE(times, float);
ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t); ret += STDVEC_MEMSIZE(m_layer_first_vertex, uint32_t);
+3
View File
@@ -416,6 +416,8 @@ set(SLIC3R_GUI_SOURCES
GUI/MultiSendMachineModel.hpp GUI/MultiSendMachineModel.hpp
GUI/MultiTaskManagerPage.cpp GUI/MultiTaskManagerPage.cpp
GUI/MultiTaskManagerPage.hpp GUI/MultiTaskManagerPage.hpp
GUI/MultiTaskModel.cpp
GUI/MultiTaskModel.hpp
GUI/NetworkTestDialog.cpp GUI/NetworkTestDialog.cpp
GUI/NetworkTestDialog.hpp GUI/NetworkTestDialog.hpp
GUI/Notebook.cpp GUI/Notebook.cpp
@@ -800,6 +802,7 @@ set(SLIC3R_GUI_SOURCES
Utils/PrintHost.hpp Utils/PrintHost.hpp
Utils/Process.cpp Utils/Process.cpp
Utils/Process.hpp Utils/Process.hpp
Utils/Profile.hpp
Utils/RaycastManager.cpp Utils/RaycastManager.cpp
Utils/RaycastManager.hpp Utils/RaycastManager.hpp
Utils/Repetier.cpp Utils/Repetier.cpp
+49 -173
View File
@@ -16,7 +16,6 @@
#include <wx/checklst.h> #include <wx/checklst.h>
#include "slic3r/GUI/GUI_Utils.hpp" #include "slic3r/GUI/GUI_Utils.hpp"
#include "slic3r/GUI/Widgets/Button.hpp" #include "slic3r/GUI/Widgets/Button.hpp"
#include "slic3r/GUI/Widgets/DropDown.hpp"
#include "slic3r/GUI/Widgets/Label.hpp" #include "slic3r/GUI/Widgets/Label.hpp"
#include <utility> #include <utility>
#include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/wxExtensions.hpp"
@@ -173,26 +172,7 @@ void AMSMaterialsSetting::create()
Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this); Bind(wxEVT_PAINT, &AMSMaterialsSetting::paintEvent, this);
Bind(EVT_SELECTED_COLOR, &AMSMaterialsSetting::on_picker_color, 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); m_comboBox_cali_result->Connect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
} }
@@ -211,9 +191,11 @@ void AMSMaterialsSetting::create_panel_normal(wxWindow* parent)
m_sizer_filament->Add(0, 0, 0, wxEXPAND, 0); 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); m_comboBox_filament = new ::ComboBox(parent, wxID_ANY, wxEmptyString, wxDefaultPosition, AMS_MATERIALS_SETTING_COMBOX_WIDTH, 0, nullptr, wxCB_READONLY);
m_star_filled = ScalableBitmap(this, "score_star_light", 16); #endif
m_star_empty = ScalableBitmap(this, "score_star_outline", 16);
m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0); m_sizer_filament->Add(m_comboBox_filament, 1, wxALIGN_CENTER, 0);
@@ -438,7 +420,6 @@ void AMSMaterialsSetting::paintEvent(wxPaintEvent &evt)
AMSMaterialsSetting::~AMSMaterialsSetting() AMSMaterialsSetting::~AMSMaterialsSetting()
{ {
m_comboBox_filament->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_filament), NULL, this); 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); m_comboBox_cali_result->Disconnect(wxEVT_COMMAND_COMBOBOX_SELECTED, wxCommandEventHandler(AMSMaterialsSetting::on_select_cali_result), NULL, this);
} }
@@ -550,9 +531,10 @@ void AMSMaterialsSetting::on_select_reset(wxCommandEvent& event) {
std::string selected_ams_id; std::string selected_ams_id;
PresetBundle *preset_bundle = wxGetApp().preset_bundle; PresetBundle *preset_bundle = wxGetApp().preset_bundle;
if (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++) { for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
if (selected_info && it->filament_id.compare(selected_info->filament_id) == 0) { 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) {
selected_ams_id = it->filament_id; selected_ams_id = it->filament_id;
break; break;
} }
@@ -700,13 +682,12 @@ void AMSMaterialsSetting::on_select_ok(wxCommandEvent &event)
ams_setting_id = ""; ams_setting_id = "";
// the combobox item // the combobox item
const FilamentInfos* selected_info = selected_filament_info(); auto filament_item = map_filament_items[m_comboBox_filament->GetValue().ToStdString()];
const std::string filament_id = selected_info ? selected_info->filament_id : std::string();
// check filament info (rack-gated per-nozzle blacklist evaluation) // check filament info (rack-gated per-nozzle blacklist evaluation)
PresetBundle* preset_bundle = wxGetApp().preset_bundle; PresetBundle* preset_bundle = wxGetApp().preset_bundle;
const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id, const auto& fila_check_res = sCheckFilamentInfo(preset_bundle, obj, ams_id, slot_id,
filament_id, ams_filament_id, ams_setting_id); filament_item.filament_id, ams_filament_id, ams_setting_id);
if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) { if (const auto& prohibit_items = fila_check_res.get_items_by_action("prohibition"); !prohibit_items.empty()) {
wxString info_msg; wxString info_msg;
@@ -1005,133 +986,6 @@ static void _collect_filament_info(const wxString& shown_name,
query_filament_types[shown_name] = filament.config.get_filament_type(); 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) void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_min, wxString temp_max, wxString k, wxString n)
{ {
if (!obj) return; if (!obj) return;
@@ -1145,12 +999,13 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
m_input_k_val->GetTextCtrl()->SetValue(k); m_input_k_val->GetTextCtrl()->SetValue(k);
m_input_n_val->GetTextCtrl()->SetValue(n); m_input_n_val->GetTextCtrl()->SetValue(n);
wxArrayString filament_items;
wxString bambu_filament_name; 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_vendors;// some information for sort
std::unordered_map<wxString, wxString> query_filament_types; // std::unordered_map<wxString, wxString> query_filament_types; //
std::set<std::string> filament_id_set; 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 // 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. // popup (a different printer, a different nozzle) would resolve to that printer's filament_id.
map_filament_items.clear(); map_filament_items.clear();
@@ -1180,7 +1035,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
if (alias.empty()) if (alias.empty())
continue; continue;
m_sorted_filament_items.push_back(alias); filament_items.push_back(alias);
_collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types); _collect_filament_info(alias, *filament_it, query_filament_vendors, query_filament_types);
FilamentInfos filament_infos; FilamentInfos filament_infos;
@@ -1189,6 +1044,7 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
map_filament_items[alias] = filament_infos; map_filament_items[alias] = filament_infos;
if (filament_it->filament_id == ams_filament_id) { if (filament_it->filament_id == ams_filament_id) {
hint_filament_name = from_u8(alias);
bambu_filament_name = from_u8(alias); bambu_filament_name = from_u8(alias);
@@ -1359,11 +1215,24 @@ void AMSMaterialsSetting::Popup(wxString filament, wxString sn, wxString temp_mi
return name_compare != 0 ? name_compare < 0 : left < right; return name_compare != 0 ? name_compare < 0 : left < right;
}; };
std::sort(m_sorted_filament_items.begin(), m_sorted_filament_items.end(), _filament_sorter); std::sort(filament_items.begin(), filament_items.end(), _filament_sorter);
} }
rebuild_filament_dropdown_items(ams_filament_id); // traverse the hint selection idx
int selection_idx = m_comboBox_filament->GetSelection(); 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);
post_select_event(selection_idx); post_select_event(selection_idx);
if (selection_idx < 0) { if (selection_idx < 0) {
@@ -1420,7 +1289,6 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData()); int* from_printer = static_cast<int*>(m_comboBox_filament->GetClientData());
m_filament_type = ""; m_filament_type = "";
const FilamentInfos* selected_info = selected_filament_info();
PresetBundle* preset_bundle = wxGetApp().preset_bundle; PresetBundle* preset_bundle = wxGetApp().preset_bundle;
if (preset_bundle) { if (preset_bundle) {
std::ostringstream stream; std::ostringstream stream;
@@ -1441,8 +1309,9 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
std::string nozzle_diameter_str = stream.str(); std::string nozzle_diameter_str = stream.str();
// Resolve the selection against the same list Popup() built the dropdown from, so the two // 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. // halves of the dialog cannot disagree about which filaments this machine can use.
if (selected_info) { const std::string selected = m_comboBox_filament->GetValue().ToStdString();
const std::string& filament_id = selected_info->filament_id; if (!selected.empty()) {
const std::string filament_id = map_filament_items[selected].filament_id;
for (Preset *it : preset_bundle->get_filament_presets_for_machine( 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)) { DevPrinterConfigUtil::get_printer_display_name(obj->printer_type), nozzle_diameter_str, obj->is_support_user_preset)) {
if (it->filament_id != filament_id) if (it->filament_id != filament_id)
@@ -1495,7 +1364,7 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
//reset cali //reset cali
int cali_select_idx = -1; int cali_select_idx = -1;
if ( !this->obj || m_filament_selection < 0 || !selected_info) { if ( !this->obj || m_filament_selection < 0) {
m_input_k_val->Enable(false); m_input_k_val->Enable(false);
m_input_n_val->Enable(false); m_input_n_val->Enable(false);
m_button_confirm->Disable(); // ORCA No need to change style m_button_confirm->Disable(); // ORCA No need to change style
@@ -1514,9 +1383,21 @@ void AMSMaterialsSetting::on_select_filament(wxCommandEvent &evt)
ams_filament_id = ""; ams_filament_id = "";
ams_setting_id = ""; ams_setting_id = "";
if (selected_info) { if (preset_bundle) {
ams_filament_id = selected_info->filament_id; for (auto it = preset_bundle->filaments.begin(); it != preset_bundle->filaments.end(); it++) {
ams_setting_id = selected_info->setting_id; 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;
}
}
} }
wxArrayString items; wxArrayString items;
@@ -1648,11 +1529,6 @@ void AMSMaterialsSetting::on_dpi_changed(const wxRect &suggested_rect)
m_button_reset->Rescale(); // ORCA m_button_reset->Rescale(); // ORCA
m_button_confirm->Rescale(); // ORCA m_button_confirm->Rescale(); // ORCA
m_button_close->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(); this->Refresh();
} }
+6 -21
View File
@@ -31,7 +31,6 @@
#include "slic3r/GUI/Widgets/StateColor.hpp" #include "slic3r/GUI/Widgets/StateColor.hpp"
#include <wx/panel.h> #include <wx/panel.h>
#include <map> #include <map>
#include <set>
#define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255) #define AMS_MATERIALS_SETTING_DEF_COLOUR wxColour(255, 255, 255)
#define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48) #define AMS_MATERIALS_SETTING_GREY900 wxColour(38, 46, 48)
@@ -157,7 +156,6 @@ protected:
void create_panel_kn(wxWindow* parent); void create_panel_kn(wxWindow* parent);
void on_dpi_changed(const wxRect &suggested_rect) override; void on_dpi_changed(const wxRect &suggested_rect) override;
void on_select_filament(wxCommandEvent& evt); void on_select_filament(wxCommandEvent& evt);
void on_toggle_favorite_filament(wxCommandEvent& evt);
void on_select_cali_result(wxCommandEvent &evt); void on_select_cali_result(wxCommandEvent &evt);
void on_select_ok(wxCommandEvent &event); void on_select_ok(wxCommandEvent &event);
void on_select_reset(wxCommandEvent &event); void on_select_reset(wxCommandEvent &event);
@@ -165,11 +163,6 @@ protected:
void on_clr_picker(wxMouseEvent &event); void on_clr_picker(wxMouseEvent &event);
bool is_virtual_tray(); bool is_virtual_tray();
void update_widgets(); 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); void update_filament_editing(bool is_printing);
// Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds // Orca: the variant index of the filament's per-variant options on the nozzle this tray feeds
@@ -209,7 +202,12 @@ protected:
int m_pa_cali_select_id = 0; 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; ComboBox *m_comboBox_filament;
#endif
ComboBox * m_comboBox_cali_result; ComboBox * m_comboBox_cali_result;
TextInput* m_readonly_filament; TextInput* m_readonly_filament;
@@ -217,20 +215,7 @@ protected:
std::string filament_id; std::string filament_id;
std::string setting_id; std::string setting_id;
}; };
struct FilamentListItem { std::map<std::string, FilamentInfos> map_filament_items;
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); wxDECLARE_EVENT(EVT_SELECTED_COLOR, wxCommandEvent);
+4 -1
View File
@@ -1,4 +1,5 @@
#pragma once #ifndef slic3r_GUI_SingleChoice_hpp_
#define slic3r_GUI_SingleChoice_hpp_
#include "GUI_Utils.hpp" #include "GUI_Utils.hpp"
#include "Plater.hpp" #include "Plater.hpp"
@@ -32,3 +33,5 @@ private:
}; };
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
#endif
+19
View File
@@ -0,0 +1,19 @@
#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
-4
View File
@@ -1105,10 +1105,6 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
toggle_field("support_filament", have_support_material || have_skirt); toggle_field("support_filament", have_support_material || have_skirt);
toggle_line("raft_contact_distance", have_raft && !have_support_soluble); toggle_line("raft_contact_distance", have_raft && !have_support_soluble);
// Orca:Hide raft expansion when raft is not enabled
toggle_line("raft_expansion", have_raft);
// Orca: Slim, Strong and Hybrid tree supports size their raft by first layer expansion only.
toggle_field("raft_expansion", !(is_tree(support_type) && (support_style == smsTreeSlim || support_style == smsTreeStrong || support_style == smsTreeHybrid)));
// Orca: First-layer density is available for supports broadly. // Orca: First-layer density is available for supports broadly.
toggle_field("raft_first_layer_density", have_support_material); toggle_field("raft_first_layer_density", have_support_material);
+3 -22
View File
@@ -1514,8 +1514,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_PRINTABLE, SimpleEvent);
const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25; const double GLCanvas3D::DefaultCameraZoomToBoxMarginFactor = 1.25;
const double GLCanvas3D::DefaultCameraZoomToBedMarginFactor = 2.00; const double GLCanvas3D::DefaultCameraZoomToBedMarginFactor = 2.00;
const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25; const double GLCanvas3D::DefaultCameraZoomToPlateMarginFactor = 1.25;
// Main-thread only: _render_frame never runs off it, so no atomic is needed.
int GLCanvas3D::s_frames_in_progress = 0;
void GLCanvas3D::load_arrange_settings() void GLCanvas3D::load_arrange_settings()
{ {
@@ -2470,8 +2468,7 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
} }
m_in_render = true; m_in_render = true;
++s_frames_in_progress; Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; });
Slic3r::ScopeGuard in_render_guard([this]() { m_in_render = false; --s_frames_in_progress; });
(void)in_render_guard; (void)in_render_guard;
if (m_canvas == nullptr) if (m_canvas == nullptr)
@@ -2716,31 +2713,15 @@ void GLCanvas3D::_render_frame(bool scene_dirty, bool only_init)
// On Wayland, eglSwapBuffers blocks when the canvas is hidden or // On Wayland, eglSwapBuffers blocks when the canvas is hidden or
// occluded. Skip the swap to avoid stalling the render loop. // occluded. Skip the swap to avoid stalling the render loop.
// Code run from inside this frame - a modal dialog a gizmo opened from its ImGui panel, a if (m_canvas->IsShownOnScreen()) {
// synchronous paint of another canvas - may have made a different surface current. Bind this one
// again before presenting, so the swap is not issued against a window the context no longer
// belongs to: on Mesa/X11 that is a fatal BadMatch, not the no-op it is on other platforms. Draws
// already issued before this point cannot be redirected - only this swap is protected.
if (m_canvas->IsShownOnScreen() && _set_current()) {
m_canvas->SwapBuffers(); m_canvas->SwapBuffers();
m_frame_profiler.mark("swap"); m_frame_profiler.mark("swap");
if (!overlay_tick) if (!overlay_tick)
m_render_stats.increment_fps_counter(); m_render_stats.increment_fps_counter();
m_presented_signature = signature; m_presented_signature = signature;
} }
else { else
if (m_canvas->IsShownOnScreen()) {
// Logged once: if the rebind fails it does so every frame, and a warning per frame would
// bury everything else.
static bool rebind_failure_logged = false;
if (!rebind_failure_logged) {
rebind_failure_logged = true;
BOOST_LOG_TRIVIAL(warning) << "GLCanvas3D: could not rebind the GL context to this canvas before "
"SwapBuffers(); the frame is not presented";
}
}
m_presented_signature.reset(); m_presented_signature.reset();
}
} }
// Everything drawn into the 3D scene, from the clear to the post processing passes, ending in the // Everything drawn into the 3D scene, from the clear to the post processing passes, ending in the
-7
View File
@@ -643,8 +643,6 @@ private:
#endif #endif
unsigned int m_last_w, m_last_h; unsigned int m_last_w, m_last_h;
bool m_in_render; bool m_in_render;
// How many canvases are inside _render_frame() right now - see is_frame_in_progress().
static int s_frames_in_progress;
wxTimer m_timer; wxTimer m_timer;
wxTimer m_timer_set_color; wxTimer m_timer_set_color;
// Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame. // Armed by each frame that draws the FPS overlay; its tick requests an overlay-only frame.
@@ -976,11 +974,6 @@ public:
void on_change_color_mode(bool is_dark, bool reinit = true); void on_change_color_mode(bool is_dark, bool reinit = true);
const bool get_dark_mode_status() { return m_is_dark; } const bool get_dark_mode_status() { return m_is_dark; }
void set_as_dirty() { m_dirty = true; } void set_as_dirty() { m_dirty = true; }
// True while any GLCanvas3D is inside _render_frame(). The other surfaces that share the app's one
// GL context (the UV editor canvas) check this before making themselves current: a frame interrupted
// by another surface finishes drawing into that surface and then presents a window the context no
// longer belongs to, which on Mesa/X11 is a fatal X error (BadMatch) rather than a no-op.
static bool is_frame_in_progress() { return s_frames_in_progress > 0; }
void requires_check_outside_state() { m_requires_check_outside_state = true; } void requires_check_outside_state() { m_requires_check_outside_state = true; }
unsigned int get_volumes_count() const { return (unsigned int)m_volumes.volumes.size(); } unsigned int get_volumes_count() const { return (unsigned int)m_volumes.volumes.size(); }
+4
View File
@@ -719,6 +719,9 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
// does not treat anti-aliased icon edges as window transparency. // does not treat anti-aliased icon edges as window transparency.
glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)); glsafe(::glBlendFuncSeparate(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, GL_ONE, GL_ONE_MINUS_SRC_ALPHA));
glsafe(::glEnable(GL_TEXTURE_2D));
glsafe(::glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_REPLACE));
glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id)); glsafe(::glBindTexture(GL_TEXTURE_2D, (GLuint)tex_id));
GLModel::Geometry init_data; GLModel::Geometry init_data;
@@ -750,6 +753,7 @@ void GLTexture::render_sub_texture(unsigned int tex_id, float left, float right,
glsafe(::glBindTexture(GL_TEXTURE_2D, 0)); glsafe(::glBindTexture(GL_TEXTURE_2D, 0));
glsafe(::glDisable(GL_TEXTURE_2D));
glsafe(::glDisable(GL_BLEND)); glsafe(::glDisable(GL_BLEND));
} }
+6 -7
View File
@@ -3232,16 +3232,9 @@ bool GUI_App::on_init_inner()
#ifdef _MSW_DARK_MODE #ifdef _MSW_DARK_MODE
#ifndef __WINDOWS__ #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(); wxSystemAppearance app = wxSystemSettings::GetAppearance();
GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0"); GUI::wxGetApp().app_config->set("dark_color_mode", app.IsDark() ? "1" : "0");
GUI::wxGetApp().app_config->save(); GUI::wxGetApp().app_config->save();
if (dark_mode() != saved_dark_mode) {
init_label_colours();
Update_dark_mode_flag();
}
#endif // __APPLE__ #endif // __APPLE__
@@ -4862,6 +4855,12 @@ void GUI_App::recreate_GUI(const wxString &msg_name)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << "recreate_GUI exit"; 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) void GUI_App::keyboard_shortcuts(ShortcutContext page, wxWindow* parent)
{ {
KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page); KBShortcutsDialog dlg(parent != nullptr ? parent : mainframe, page);
+1
View File
@@ -519,6 +519,7 @@ public:
void check_printer_presets(); void check_printer_presets();
void recreate_GUI(const wxString& message); void recreate_GUI(const wxString& message);
void system_info();
void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null void keyboard_shortcuts(ShortcutContext page, wxWindow* parent = nullptr); // the main frame when null
void troubleshoot(); void troubleshoot();
void load_project(wxWindow *parent, wxString& input_file) const; void load_project(wxWindow *parent, wxString& input_file) const;
-10
View File
@@ -1952,16 +1952,6 @@ void MenuFactory::create_plate_menu()
}, },
"", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent); "", nullptr, []() { return !plater()->get_partplate_list().get_selected_plate()->get_objects().empty(); }, m_parent);
// fix objects on current plate
append_menu_item(menu, wxID_ANY, _L("Fix All"), _L("Fix all models on the current plate"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(false); }, "", nullptr,
[]() { return !plater()->get_partplate_list().get_selected_plate()->empty(); }, m_parent);
// fix objects on all plates
append_menu_item(menu, wxID_ANY, _L("Fix All Plates"), _L("Fix all models on all plates"),
[](wxCommandEvent&) { obj_list()->fix_all_through_cgal(true); }, "", nullptr,
[]() { return !plater()->model().objects.empty(); }, m_parent);
// orient objects on current plate // orient objects on current plate
append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"), append_menu_item(menu, wxID_ANY, _L("Auto Rotate"), _L("Auto rotate current plate"),
[](wxCommandEvent&) { [](wxCommandEvent&) {
+65 -78
View File
@@ -1617,7 +1617,7 @@ void ObjectList::list_manipulation(const wxPoint& mouse_pos, bool evt_context_me
int obj_idx, vol_idx; int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item); get_selected_item_indexes(obj_idx, vol_idx, item);
if (obj_idx != -1) { if (obj_idx != -1) {
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed")); Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
(*m_objects)[obj_idx]->ensure_on_bed(); (*m_objects)[obj_idx]->ensure_on_bed();
cnv->reload_scene(true, true); cnv->reload_scene(true, true);
update_info_items(obj_idx); update_info_items(obj_idx);
@@ -2779,13 +2779,12 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
GLCanvas3D* cnv = plater->canvas3D(); GLCanvas3D* cnv = plater->canvas3D();
switch (type) { switch (type) {
case InfoItemType::CustomSupports: { case InfoItemType::CustomSupports:
cnv->get_gizmos_manager().reset_all_states(); cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot snapshot(plater, "Remove support painting"); Plater::TakeSnapshot(plater, "Remove support painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->supported_facets.reset(); mv->supported_facets.reset();
break; break;
}
// BBS: remove CustomSeam // BBS: remove CustomSeam
@@ -2796,21 +2795,19 @@ void ObjectList::del_info_item(const int obj_idx, InfoItemType type)
} }
break; break;
case InfoItemType::MmSegmentation: { case InfoItemType::MmSegmentation:
cnv->get_gizmos_manager().reset_all_states(); cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot snapshot(plater, "Remove color painting"); Plater::TakeSnapshot(plater, "Remove color painting");
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->mmu_segmentation_facets.reset(); mv->mmu_segmentation_facets.reset();
break; break;
}
case InfoItemType::FuzzySkin: { case InfoItemType::FuzzySkin:
cnv->get_gizmos_manager().reset_all_states(); cnv->get_gizmos_manager().reset_all_states();
Plater::TakeSnapshot snapshot(plater, _u8L("Remove paint-on fuzzy skin")); Plater::TakeSnapshot(plater, _u8L("Remove paint-on fuzzy skin"));
for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes) for (ModelVolume* mv : (*m_objects)[obj_idx]->volumes)
mv->fuzzy_skin_facets.reset(); mv->fuzzy_skin_facets.reset();
break; break;
}
// BBS: remove Sinking // BBS: remove Sinking
case InfoItemType::Undef : assert(false); break; case InfoItemType::Undef : assert(false); break;
@@ -6297,44 +6294,30 @@ void ObjectList::rename_item()
} }
void ObjectList::fix_through_cgal() void ObjectList::fix_through_cgal()
{
std::vector<int> obj_idxs, vol_idxs;
get_selection_indexes(obj_idxs, vol_idxs);
fix_through_cgal(obj_idxs, vol_idxs);
}
void ObjectList::fix_all_through_cgal(bool all_plates)
{
std::vector<int> obj_idxs;
if (all_plates) {
for (int obj_idx = 0; obj_idx < int(m_objects->size()); ++obj_idx)
obj_idxs.push_back(obj_idx);
} else {
for (const ModelObject *mo : wxGetApp().plater()->get_partplate_list().get_selected_plate()->get_objects_on_this_plate())
obj_idxs.push_back(int(std::find(m_objects->begin(), m_objects->end(), mo) - m_objects->begin()));
std::sort(obj_idxs.begin(), obj_idxs.end());
obj_idxs.erase(std::unique(obj_idxs.begin(), obj_idxs.end()), obj_idxs.end());
}
fix_through_cgal(obj_idxs, {});
}
void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs)
{ {
// Do not fix anything when a gizmo is open. There might be issues with updates // Do not fix anything when a gizmo is open. There might be issues with updates
// and what is worse, the snapshot time would refer to the internal stack. // and what is worse, the snapshot time would refer to the internal stack.
if (obj_idxs.empty() || !wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined)) if (!wxGetApp().plater()->get_view3D_canvas3D()->get_gizmos_manager().check_gizmos_closed_except(GLGizmosManager::Undefined))
return; return;
// model_name // model_name
std::vector<std::string> succes_models; std::vector<std::string> succes_models;
// model_name failing reason // model_name failing reason
std::vector<std::pair<std::string, std::string>> failed_models; std::vector<std::pair<std::string, std::string>> failed_models;
size_t unchanged_models = 0;
bool painting_removed = false; std::vector<int> obj_idxs, vol_idxs;
get_selection_indexes(obj_idxs, vol_idxs);
std::vector<std::string> model_names; std::vector<std::string> model_names;
// clear selections from the non-broken models if any exists
// and than fill names of models to repairing
if (vol_idxs.empty()) { if (vol_idxs.empty()) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(obj_idxs.size())-1; i >= 0; --i)
if (object(obj_idxs[i])->get_repaired_errors_count() == 0)
obj_idxs.erase(obj_idxs.begin()+i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int obj_idx : obj_idxs) for (int obj_idx : obj_idxs)
if (object(obj_idx)) if (object(obj_idx))
model_names.push_back(object(obj_idx)->name); model_names.push_back(object(obj_idx)->name);
@@ -6342,26 +6325,31 @@ void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::v
else { else {
ModelObject* obj = object(obj_idxs.front()); ModelObject* obj = object(obj_idxs.front());
if (obj) { if (obj) {
#if !FIX_THROUGH_CGAL_ALWAYS
for (int i = int(vol_idxs.size()) - 1; i >= 0; --i)
if (obj->get_repaired_errors_count(vol_idxs[i]) == 0)
vol_idxs.erase(vol_idxs.begin() + i);
#endif // FIX_THROUGH_CGAL_ALWAYS
for (int vol_idx : vol_idxs) for (int vol_idx : vol_idxs)
model_names.push_back(obj->volumes[vol_idx]->name); model_names.push_back(obj->volumes[vol_idx]->name);
} }
} }
auto plater = wxGetApp().plater(); auto plater = wxGetApp().plater();
const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
auto fix_and_update_progress = [&](const int obj_idx, const int vol_idx, int model_idx, ProgressDialog& progress_dlg) auto fix_and_update_progress = [this, plater, model_names](const int obj_idx, const int vol_idx,
int model_idx,
ProgressDialog& progress_dlg,
std::vector<std::string>& succes_models,
std::vector<std::pair<std::string, std::string>>& failed_models)
{ {
ModelObject *mo = object(obj_idx); if (!object(obj_idx))
if (!mo)
return false; return false;
const std::string& model_name = model_names[model_idx]; const std::string& model_name = model_names[model_idx];
wxString msg = _L("Repairing model object"); wxString msg = _L("Repairing model object");
if (model_names.size() == 1) if (model_names.size() == 1)
msg += ": " + from_u8(model_name) + "\n"; msg += ": " + from_u8(model_name) + "\n";
else if (model_names.size() > 10)
msg += ": " + from_u8(model_name) + wxString::Format(" (%d/%d)", model_idx + 1, int(model_names.size())) + "\n";
else { else {
msg += ":\n"; msg += ":\n";
for (int i = 0; i < int(model_names.size()); ++i) for (int i = 0; i < int(model_names.size()); ++i)
@@ -6369,29 +6357,33 @@ void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::v
msg += "\n"; msg += "\n";
} }
auto count_painted = [mo]() { return std::count_if(mo->volumes.begin(), mo->volumes.end(), [](const ModelVolume *mv) { return mv->is_any_painted(); }); }; const bool keep_painting = GUI::wxGetApp().app_config->get_bool("keep_painting");
const auto painted_before = count_painted(); if (!keep_painting) {
std::string res; plater->clear_before_change_mesh(obj_idx);
bool modified = false;
if (!fix_model_with_cgal_gui(*mo, vol_idx, progress_dlg, msg, res, keep_painting, &modified))
return false;
if (modified) {
painting_removed = painting_removed || count_painted() < painted_before;
mo->ensure_on_bed();
plater->changed_mesh(obj_idx);
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
plater->sidebar().obj_list()->update_plate_values_for_items();
update_item_error_icon(obj_idx, vol_idx);
update_info_items(obj_idx);
} }
const size_t volumes_before = object(obj_idx)->volumes.size();
std::string res;
if (!fix_model_with_cgal_gui(*(object(obj_idx)), vol_idx, progress_dlg, msg, res, keep_painting))
return false;
//wxGetApp().plater()->changed_mesh(obj_idx);
object(obj_idx)->ensure_on_bed();
plater->changed_mesh(obj_idx);
if (!res.empty()) const size_t volumes_after = object(obj_idx)->volumes.size();
failed_models.push_back({ model_name, res }); if (volumes_after != volumes_before)
else if (modified) add_volumes_to_object_in_list(obj_idx);
plater->get_partplate_list().notify_instance_update(obj_idx, 0);
plater->sidebar().obj_list()->update_plate_values_for_items();
if (res.empty())
succes_models.push_back(model_name); succes_models.push_back(model_name);
else else
++unchanged_models; failed_models.push_back({ model_name, res });
update_item_error_icon(obj_idx, vol_idx);
update_info_items(obj_idx);
return true; return true;
}; };
@@ -6404,7 +6396,11 @@ void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::v
if (vol_idxs.empty()) { if (vol_idxs.empty()) {
int vol_idx{ -1 }; int vol_idx{ -1 };
for (int obj_idx : obj_idxs) { for (int obj_idx : obj_idxs) {
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg)) #if !FIX_THROUGH_CGAL_ALWAYS
if (object(obj_idx)->get_repaired_errors_count(vol_idx) == 0)
continue;
#endif // FIX_THROUGH_CGAL_ALWAYS
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
break; break;
model_idx++; model_idx++;
} }
@@ -6412,7 +6408,7 @@ void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::v
else { else {
int obj_idx{ obj_idxs.front() }; int obj_idx{ obj_idxs.front() };
for (int vol_idx : vol_idxs) { for (int vol_idx : vol_idxs) {
if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg)) if (!fix_and_update_progress(obj_idx, vol_idx, model_idx, progress_dlg, succes_models, failed_models))
break; break;
model_idx++; model_idx++;
} }
@@ -6420,29 +6416,20 @@ void ObjectList::fix_through_cgal(const std::vector<int> &obj_idxs, const std::v
// Close the progress dialog // Close the progress dialog
progress_dlg.Update(100, ""); progress_dlg.Update(100, "");
if (painting_removed)
plater->notify_painting_removed();
// Show info notification // Show info notification
std::vector<wxString> blocks; wxString msg;
wxString bullet_suf = "\n - "; wxString bullet_suf = "\n - ";
if (!succes_models.empty()) { if (!succes_models.empty()) {
wxString block = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":"; msg = _L_PLURAL("The following model object has been repaired", "The following model objects have been repaired", succes_models.size()) + ":";
for (auto& model : succes_models) for (auto& model : succes_models)
block += bullet_suf + from_u8(model); msg += bullet_suf + from_u8(model);
blocks.push_back(block); msg += "\n\n";
} }
if (!failed_models.empty()) { if (!failed_models.empty()) {
wxString block = _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":"; msg += _L_PLURAL("Failed to repair the following model object", "Failed to repair the following model objects", failed_models.size()) + ":\n";
for (auto& model : failed_models) for (auto& model : failed_models)
block += bullet_suf + from_u8(model.first) + ": " + _(model.second); msg += bullet_suf + from_u8(model.first) + ": " + _(model.second);
blocks.push_back(block);
} }
if (unchanged_models > 0)
blocks.push_back(format_wxstr(_L_PLURAL("%1$d model object needed no repair.", "%1$d model objects needed no repair.", unchanged_models), unchanged_models));
wxString msg;
for (const wxString &block : blocks)
msg += (msg.IsEmpty() ? "" : "\n\n") + block;
if (msg.IsEmpty()) if (msg.IsEmpty())
msg = _L("Repairing was canceled"); msg = _L("Repairing was canceled");
plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg)); plater->get_notification_manager()->push_notification(NotificationType::CgalFinished, NotificationManager::NotificationLevel::PrintInfoShortNotificationLevel, into_u8(msg));
@@ -6731,7 +6718,7 @@ void ObjectList::OnEditingStarted(wxDataViewEvent &event)
else if (col == colSinking) { else if (col == colSinking) {
Plater * plater = wxGetApp().plater(); Plater * plater = wxGetApp().plater();
GLCanvas3D *cnv = plater->canvas3D(); GLCanvas3D *cnv = plater->canvas3D();
Plater::TakeSnapshot snapshot(plater, _u8L("Shift objects to bed")); Plater::TakeSnapshot(plater, _u8L("Shift objects to bed"));
int obj_idx, vol_idx; int obj_idx, vol_idx;
get_selected_item_indexes(obj_idx, vol_idx, item); get_selected_item_indexes(obj_idx, vol_idx, item);
(*m_objects)[obj_idx]->ensure_on_bed(); (*m_objects)[obj_idx]->ensure_on_bed();
+3 -3
View File
@@ -47,6 +47,9 @@ typedef double coordf_t;
typedef std::pair<coordf_t, coordf_t> t_layer_height_range; typedef std::pair<coordf_t, coordf_t> t_layer_height_range;
typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges; typedef std::map<t_layer_height_range, ModelConfig> t_layer_config_ranges;
// Manifold mesh may contain self-intersections, so we want to always allow fixing the mesh.
#define FIX_THROUGH_CGAL_ALWAYS 1
namespace GUI { namespace GUI {
enum class Shortcut : uint8_t; enum class Shortcut : uint8_t;
@@ -460,9 +463,6 @@ public:
void split_instances(); void split_instances();
void rename_item(); void rename_item();
void fix_through_cgal(); void fix_through_cgal();
// Fixes every object on the selected plate, or on all plates.
void fix_all_through_cgal(bool all_plates);
void fix_through_cgal(const std::vector<int> &obj_idxs, const std::vector<int> &vol_idxs);
void simplify(); void simplify();
void smooth_mesh(); void smooth_mesh();
void update_item_error_icon(const int obj_idx, int vol_idx) const ; void update_item_error_icon(const int obj_idx, int vol_idx) const ;
@@ -1036,11 +1036,6 @@ bool GLGizmoPainterBase::on_mouse(const wxMouseEvent &mouse_event)
Vec2d mouse_pos = mouse_coord.cast<double>(); Vec2d mouse_pos = mouse_coord.cast<double>();
if (mouse_event.Moving()) { if (mouse_event.Moving()) {
// No button is down, yet a stroke is still open: its release went elsewhere, as a paint drag does not
// capture the mouse. End the stroke as that release would have.
if (m_button_down != Button::None)
gizmo_event(m_button_down == Button::Left ? SLAGizmoEventType::LeftUp : SLAGizmoEventType::RightUp, mouse_pos,
mouse_event.ShiftDown(), mouse_event.AltDown(), false);
gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false); gizmo_event(SLAGizmoEventType::Moving, mouse_pos, mouse_event.ShiftDown(), mouse_event.AltDown(), false);
return false; return false;
} }
@@ -1210,10 +1205,6 @@ void GLGizmoPainterBase::on_set_state()
m_old_mo_id = -1; m_old_mo_id = -1;
//m_iva.release_geometry(); //m_iva.release_geometry();
m_triangle_selectors.clear(); m_triangle_selectors.clear();
// A stroke still open goes with the selectors. Its state must not carry into the next session, where the
// first move would end it as if released and the first press would paint a line from its last position.
m_button_down = Button::None;
m_last_mouse_click = Vec2d::Zero();
//Camera& camera = wxGetApp().plater()->get_camera(); //Camera& camera = wxGetApp().plater()->get_camera();
//camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ()); //camera.look_at(camera.get_position(), m_previous_target, Vec3d::UnitZ());
File diff suppressed because it is too large Load Diff
@@ -23,7 +23,6 @@
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include <imgui.h> #include <imgui.h>
#include <map> #include <map>
#include <future>
#include <memory> #include <memory>
#include <optional> #include <optional>
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp" #include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
@@ -198,27 +197,6 @@ public:
}; };
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint); static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
// The view while the UV editor's selected islands are highlighted on the model. The highlight lies on the
// undisplaced surface, so the Normal view's relief hides it wherever it rises: Normal gives way to Fast
// while islands are selected and comes back once none are. A view the user clicks in the View row is
// theirs - the pane's background buttons are not a pick, Height only meaning "the texture view".
struct IslandViewFollow
{
bool shown = false; // the highlight, as of the last step()
bool switched = false; // Fast was switched on for the highlight, so Normal comes back once it is gone
bool keep_normal = false; // the user picked Normal in the View row while the highlight was shown
// The view to apply this frame (0 Normal, 1 Fast), or -1 for none. `view` is the one on screen, numbered as
// apply_view_mode() takes it; `debug_open` a debug stage on screen, which holds Normal itself.
int step(bool shown_now, int view, bool debug_open);
// A View row button clicked, the active one included. The row is drawn after step() in the same frame,
// so `shown` is this frame's.
void picked(int view)
{
switched = false;
keep_normal = shown && view == 0;
}
};
void render_painter_gizmo() override; void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/ // Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
@@ -284,9 +262,6 @@ private:
// Standard is active so what Preview shows is always what Bake will do. Returns true if it actually // 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. // changed something, so the caller can invalidate the preview.
bool apply_standard_mode_presets(ModelVolume *mv); 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. // 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 // 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 // existing vertices, so the mesh has to be prepared first, and remeshing after painting would drop
@@ -429,77 +404,6 @@ private:
// Per slot: whether the layer's card shows every setting or only Depth, Tile size and Rotation. // 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. // Panel state only, not saved with the project.
std::array<bool, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_layer_expanded{}; 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;
// ---- The image-adjustment panel ----
//
// Brightness/contrast/sharpness/blur/hue over one layer's image, shown on the image itself and
// updating as the sliders move. What it produces is not a set of layer parameters: it replaces the
// layer's *image*, so the adjusted pixels feed the colour and the displacement together and a
// brightness change moves the relief too (see TextureAdjustments).
//
// An ordinary floating window rather than a popup, and deliberately so. A popup dims the canvas
// behind it and closes on any click outside itself, which between them make it impossible to watch the
// relief change on the model - which is the whole point of adjusting the image here rather than in an
// image editor. A window leaves the 3D view live, clickable and orbitable while the panel is open.
void render_image_adjust_window(const ImVec2 &panel_min, const ImVec2 &panel_max);
// Opens the panel for `layer`, decoding its image into m_adjust_source. Sliders never re-decode
// the PNG.
void open_image_adjust(const TextureDisplacementLayer &layer);
// Reads `layer`'s stored image into the panel's source buffers and records which image it came from.
// Called when the panel opens and again whenever the layer's image is swapped under it, which is why
// it leaves the slider values alone. False when the image cannot be decoded.
bool load_image_adjust_source(const TextureDisplacementLayer &layer);
// Resolves the panel: writes the adjusted image into the user's texture library as a new file and
// points `layer` at it, which is the only way an adjustment becomes permanent. Closes the panel.
void commit_image_adjust(TextureDisplacementLayer &layer);
// Closes the panel and reverts the layer's live preview_adjust to identity, so every exit that is not
// a commit - Cancel, the window's close button, the gizmo closing - leaves the layer as it was found.
void close_image_adjust();
// Collects a finished preview and starts one for the current slider values. One at a time, so a drag
// coalesces into "whatever the sliders last said" instead of a queue of superseded frames.
void update_image_adjust_preview();
int m_adjust_slot = -1; // the layer the panel is open for; -1 when closed
TextureAdjustments m_adjust; // what the sliders currently say
// What the layer's live preview_adjust was last set from, so a slider move can be told from a frame in
// which nothing happened and only the former asks the displacement to be recomputed.
TextureAdjustments m_adjust_applied;
// The layer's image as it was when the panel opened, and a small copy of it. The sliders are applied
// to the small copy for the live preview and to the full-size one exactly once, on commit. Measured on
// a 1024x1024 with every knob engaged, a full-size pass costs 13-19 ms grayscale and 40-60 ms in
// colour (130-190 ms at 2048 in colour) - far too much to repeat per slider frame. The same pass on a
// 384 px copy is 3-4 ms / 9-12 ms, and that is all the preview box can show anyway.
DecodedHeightTexture m_adjust_source;
DecodedHeightTexture m_adjust_preview_source;
// Which image_data the two buffers above were decoded from, so the panel can notice the layer's image
// being swapped from the texture picker while it is open and re-read it. Compared, never dereferenced.
const void *m_adjust_source_image = nullptr;
// And which volume the panel was opened on. Slot numbers are per volume, so without this a change of
// selection would silently rebind the open panel to whichever layer happens to hold the same slot on
// the newly selected object - a different image, under a panel the user opened for this one.
size_t m_adjust_volume_id = 0;
// The preview image on screen, re-uploaded whenever a background pass finishes.
std::unique_ptr<GLTexture> m_adjust_preview_tex;
// The live preview runs on its own thread rather than on the UI thread or on the plater's job
// worker. Not the UI thread because even the small pass is milliseconds and this runs while a slider
// is being dragged; not the job worker because that is a single FIFO shared with Bake and the
// displacement preview (see queue_preview_job()), where a per-frame image filter would sit behind -
// and delay - multi-second mesh work.
std::future<DecodedHeightTexture> m_adjust_preview_job;
TextureAdjustments m_adjust_preview_running; // what the in-flight pass is computing
TextureAdjustments m_adjust_preview_shown; // what m_adjust_preview_tex holds
bool m_adjust_preview_valid = false;
// Drops the GPU copies of the layer's image so the next frame rebuilds them from the decode, which
// is where the live adjustment is applied. Called whenever the panel moves or reverts it.
void invalidate_adjust_textures();
// "Adjust Texture" mode: instead of painting, dragging an on-canvas handle changes the active // "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 // layer's offset. The handle is a flat panel lying in the paint patch's own tangent plane
@@ -519,8 +423,8 @@ private:
// obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's // obvious whether the next stroke adds paint (default) or erases it (Shift). Uses ImGui's
// foreground draw list, so it must be called from inside the gizmo's ImGui frame. // foreground draw list, so it must be called from inside the gizmo's ImGui frame.
void render_paint_cursor_hint(); void render_paint_cursor_hint();
// Mesh-local tangent-plane basis at m_adjust_anchor_normal: box_projection_axes(), so dragging // Mesh-local tangent-plane basis at m_adjust_anchor_normal, matching project_planar()'s
// on-canvas maps consistently onto offset whichever face the anchor is on. // dominant-axis convention so dragging on-canvas maps consistently onto offset.
void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const; void adjust_tangent_basis(Vec3f &u_axis, Vec3f &v_axis) const;
// The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface. // The plane a drag is measured against: the paint patch's anchor, lifted clear of the surface.
@@ -600,11 +504,6 @@ private:
// The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's // The panel's view modes: 0 Normal, 1 Fast, 2 Checker, 3 Distortion. Shared by the View row and the pane's
// background buttons, so both switch views the same way. // background buttons, so both switch views the same way.
void apply_view_mode(int mode); void apply_view_mode(int mode);
// The view on screen, numbered as apply_view_mode() takes it.
int view_mode() const;
// Run once a frame, after the pane's commands: switches the view as m_island_view says.
void follow_island_highlight();
IslandViewFollow m_island_view;
// The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes. // The pane header's thumbnail of the active layer's texture, rebuilt only when its image changes.
static constexpr int UV_THUMB_PX = 64; static constexpr int UV_THUMB_PX = 64;
const void *m_uv_thumb_source = nullptr; const void *m_uv_thumb_source = nullptr;
@@ -836,8 +735,6 @@ private:
// in place. Rebuilt whenever the pane's selection differs from the one it was built for. // in place. Rebuilt whenever the pane's selection differs from the one it was built for.
GLModel m_island_overlay_glmodel; GLModel m_island_overlay_glmodel;
std::vector<int> m_island_overlay_selection; std::vector<int> m_island_overlay_selection;
// The pane's selection while the overlay applies - the pane open over an unwrapped LSCM layer - else null.
const std::vector<int> *island_highlight_selection();
void rebuild_island_overlay(const std::vector<int> &selection); void rebuild_island_overlay(const std::vector<int> &selection);
void render_island_overlay(); void render_island_overlay();
// Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept // Set on every paint event, cleared when the overlay is rebuilt in render_painter_gizmo(). Kept
@@ -981,7 +878,6 @@ private:
enum class UVBackground { None, Height, Checker }; enum class UVBackground { None, Height, Checker };
UVBackground m_uv_editor_bg = UVBackground::None; UVBackground m_uv_editor_bg = UVBackground::None;
float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at float m_uv_editor_bg_smoothing = -1.f; // smoothing the height backdrop was uploaded at
TextureAdjustments m_uv_editor_bg_adjust; // and the live image adjustment it was uploaded at
const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from const void *m_uv_editor_bg_image = nullptr; // the layer image it was uploaded from
// Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is // Per-chart distortion heatmap colour for the UV pane (#7/#14), computed once when the unwrap is
// re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only // re-solved (relative stretch doesn't change when islands are merely moved), fed to the canvas only
@@ -1019,26 +915,25 @@ private:
bool m_preview_job_pending = false; bool m_preview_job_pending = false;
void queue_preview_job(); void queue_preview_job();
// Per-slot GPU thumbnail cache for the layer list panel, each keyed by the image_data it was built from // Per-slot GPU thumbnail cache for the layer list panel, keyed by the image_data pointer that
// (see get_layer_thumbnail()). Held weakly rather than by address, as decode_height_texture()'s cache is: // was current the last time each thumbnail was built (see get_layer_thumbnail()).
// an image freed and replaced by another at the same address must not pass for it. std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails;
std::array<std::unique_ptr<GLTexture>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnails; std::array<const void *, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source{};
std::array<std::weak_ptr<const std::vector<unsigned char>>, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_source;
// The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it. // The smoothing each cached thumbnail was built at, so a smoothing change re-uploads it.
std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{}; std::array<float, TEXTURE_DISPLACEMENT_MAX_LAYERS> m_thumbnail_smoothing{};
// Full-resolution height texture for the preview shader, keyed the same way (see // Full-resolution height texture for the preview shader, keyed the same way (see
// get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the // get_layer_height_texture()). A smoothing change re-uploads it, so the fast preview shows the
// blur the bake will apply. // blur the bake will apply.
std::unique_ptr<GLTexture> m_height_tex; std::unique_ptr<GLTexture> m_height_tex;
std::weak_ptr<const std::vector<unsigned char>> m_height_tex_source; const void *m_height_tex_source = nullptr;
float m_height_tex_smoothing = -1.f; float m_height_tex_smoothing = -1.f;
// The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows // The same, for the layer's *colour*, which the fast preview quantizes per fragment so it shows
// the image at texel resolution rather than at the mesh's. Null for a grayscale texture. // the image at texel resolution rather than at the mesh's. Null for a grayscale texture.
std::unique_ptr<GLTexture> m_color_tex; std::unique_ptr<GLTexture> m_color_tex;
std::weak_ptr<const std::vector<unsigned char>> m_color_tex_source; const void *m_color_tex_source = nullptr;
float m_color_tex_smoothing = -1.f; float m_color_tex_smoothing = -1.f;
// Library textures the picker has shown at least once, keyed by file path (see LibraryTexture). // Library textures the picker has shown at least once, keyed by file path (see LibraryTexture).
std::map<std::string, LibraryTexture> m_library_textures; std::map<std::string, LibraryTexture> m_library_textures;
@@ -1076,12 +971,9 @@ private:
PatchUnwrap m_uv_editor_unwrap; PatchUnwrap m_uv_editor_unwrap;
// When set, the panel is a free-floating window the user can drag anywhere (with a title bar to // 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. Kept in the application // grab), instead of being pinned to the right of the gizmo toolbar. Persisted across gizmo
// config rather than in the project: it describes the user's window, not the model, so opening // open/close within a session, so the choice sticks while working.
// someone else's project must not move their panel.
bool m_undocked = false; 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). // 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 // ImGui jumps a fixed number of lines per wheel notch, which on tall layer cards reads as a hard
@@ -1124,9 +1016,6 @@ private:
std::vector<std::pair<wxString, wxString>> m_shortcuts_bucket_fill; 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")} // 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; 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 // 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 // the same colour/monochrome variants the main toolbar uses, so an inactive button shows the icon in
+5 -13
View File
@@ -420,10 +420,7 @@ void GLGizmosManager::refresh_on_off_state()
void GLGizmosManager::reset_all_states() void GLGizmosManager::reset_all_states()
{ {
// The restore/new-project event can run before deferred OpenGL resource if (! m_enabled || m_serializing)
// loading has populated the gizmo list. There is nothing to reset until
// initialization completes.
if (! m_enabled || m_serializing || m_gizmos.empty())
return; return;
const EType current = get_current_type(); const EType current = get_current_type();
@@ -438,9 +435,6 @@ void GLGizmosManager::reset_all_states()
bool GLGizmosManager::open_gizmo(EType type) bool GLGizmosManager::open_gizmo(EType type)
{ {
if (m_gizmos.empty())
return false;
int idx = static_cast<int>(type); int idx = static_cast<int>(type);
// re-open same type cause closing // re-open same type cause closing
@@ -528,7 +522,7 @@ bool GLGizmosManager::is_running() const
bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut) bool GLGizmosManager::open_gizmo_by_shortcut(Shortcut shortcut)
{ {
if (!m_enabled || m_gizmos.empty()) if (!m_enabled)
return false; return false;
// The text tool opens without a selection because it creates its own object. // The text tool opens without a selection because it creates its own object.
@@ -1433,12 +1427,10 @@ void GLGizmosManager::update_hover_state(const EType &type)
bool GLGizmosManager::activate_gizmo(EType type) bool GLGizmosManager::activate_gizmo(EType type)
{ {
// already activated assert(!m_gizmos.empty());
if (m_current == type)
return true;
if (m_gizmos.empty()) // already activated
return false; if (m_current == type) return true;
if (m_current != Undefined) { if (m_current != Undefined) {
// clean up previous gizmo // clean up previous gizmo
@@ -925,9 +925,8 @@ void GizmoObjectManipulation::do_render_move_window(ImGuiWrapper *imgui_wrapper,
m_last_active_item = current_active_id; m_last_active_item = current_active_id;
last_move_input_window_width = ImGui::GetWindowWidth(); last_move_input_window_width = ImGui::GetWindowWidth();
ImGui::PopStyleVar();
imgui_wrapper->end(); imgui_wrapper->end();
ImGui::PopStyleVar(); ImGui::PopStyleVar(2);
ImGuiWrapper::pop_toolbar_style(); ImGuiWrapper::pop_toolbar_style();
} }
@@ -1132,11 +1131,10 @@ void GizmoObjectManipulation::do_render_rotate_window(ImGuiWrapper *imgui_wrappe
m_last_active_item = current_active_id; m_last_active_item = current_active_id;
last_rotate_input_window_width = ImGui::GetWindowWidth(); last_rotate_input_window_width = ImGui::GetWindowWidth();
ImGui::PopStyleVar();
imgui_wrapper->end(); imgui_wrapper->end();
// BBS // BBS
ImGui::PopStyleVar(); ImGui::PopStyleVar(2);
ImGuiWrapper::pop_toolbar_style(); ImGuiWrapper::pop_toolbar_style();
} }
@@ -8,7 +8,6 @@
#include "libslic3r/TextureDisplacement.hpp" #include "libslic3r/TextureDisplacement.hpp"
#include <exception> #include <exception>
#include <cstddef> #include <cstddef>
#include <cstdint>
#include "slic3r/GUI/Jobs/Worker.hpp" #include "slic3r/GUI/Jobs/Worker.hpp"
#include <memory> #include <memory>
@@ -27,7 +26,6 @@
#include "slic3r/Utils/UndoRedo.hpp" #include "slic3r/Utils/UndoRedo.hpp"
#include "libslic3r/ObjectID.hpp" #include "libslic3r/ObjectID.hpp"
#include "libslic3r/TriangleMesh.hpp" #include "libslic3r/TriangleMesh.hpp"
#include <array>
namespace Slic3r::GUI { namespace Slic3r::GUI {
@@ -128,71 +126,16 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
{ mix.a_percent(), 100 - mix.a_percent() }); { 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_mesh(std::move(m_result));
volume->set_new_unique_id(); volume->set_new_unique_id();
volume->calculate_convex_hull(); 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 // 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, // 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 // 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 // filament". That is what confines the effect to the painted area without having to invent a
// colour for everything outside it. What was already painted is whatever the transfer above // colour for everything outside it. Safe to index straight onto the new mesh - the bake is
// left in the annotation, so this bake's colour lands on top of it rather than instead of it. // topology-preserving, so triangle i is still triangle i.
if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) { if (!m_triangle_color.empty() && m_triangle_color.size() == volume->mesh().its.indices.size()) {
TriangleSelector selector(volume->mesh()); TriangleSelector selector(volume->mesh());
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data(); const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
@@ -216,6 +159,7 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
// pipeline, which rebuilds and then simplifies the mesh. A mask left behind against the old // 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 // 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(). // 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) { 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(), const auto it = std::find_if(m_input.layers.begin(), m_input.layers.end(),
[slot](const TextureDisplacementLayer &l) { return l.slot == slot; }); [slot](const TextureDisplacementLayer &l) { return l.slot == slot; });
+6
View File
@@ -0,0 +1,6 @@
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
+11
View File
@@ -0,0 +1,11 @@
#ifndef slic3r_MultiTaskModel_hpp_
#define slic3r_MultiTaskModel_hpp_
namespace Slic3r {
namespace GUI {
} // namespace GUI
} // namespace Slic3r
#endif
+7
View File
@@ -1880,6 +1880,13 @@ void PartPlate::render_exclude_area(bool force_default_color) {
// draw exclude area // draw exclude area
glsafe(::glDepthMask(GL_FALSE)); glsafe(::glDepthMask(GL_FALSE));
if (m_selected) {
glsafe(::glColor4fv(select_color.data()));
}
else {
glsafe(::glColor4fv(unselect_color.data()));
}
m_exclude_triangles.set_color(m_selected ? select_color : unselect_color); m_exclude_triangles.set_color(m_selected ? select_color : unselect_color);
m_exclude_triangles.render(); m_exclude_triangles.render();
glsafe(::glDepthMask(GL_TRUE)); glsafe(::glDepthMask(GL_TRUE));
+24 -11
View File
@@ -14984,8 +14984,24 @@ bool Plater::priv::can_fix_through_cgal() const
std::vector<int> obj_idxs, vol_idxs; std::vector<int> obj_idxs, vol_idxs;
sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs); sidebar->obj_list()->get_selection_indexes(obj_idxs, vol_idxs);
// A closed mesh may still self-intersect, so fixing is always allowed. #if FIX_THROUGH_CGAL_ALWAYS
// Fixing always.
return ! obj_idxs.empty() || ! vol_idxs.empty(); return ! obj_idxs.empty() || ! vol_idxs.empty();
#else // FIX_THROUGH_CGAL_ALWAYS
// Fixing only if the model is not manifold.
if (vol_idxs.empty()) {
for (auto obj_idx : obj_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count() > 0)
return true;
return false;
}
int obj_idx = obj_idxs.front();
for (auto vol_idx : vol_idxs)
if (model.objects[obj_idx]->get_repaired_errors_count(vol_idx) > 0)
return true;
return false;
#endif // FIX_THROUGH_CGAL_ALWAYS
} }
bool Plater::priv::can_simplify() const bool Plater::priv::can_simplify() const
@@ -22025,16 +22041,13 @@ void Plater::clear_before_change_mesh(int obj_idx)
mv->mmu_segmentation_facets.reset(); mv->mmu_segmentation_facets.reset();
mv->fuzzy_skin_facets.reset(); mv->fuzzy_skin_facets.reset();
} }
if (paint_removed) if (paint_removed) {
notify_painting_removed(); // snapshot_time is captured by copy so the lambda knows where to undo/redo to.
} get_notification_manager()->push_notification(
NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
void Plater::notify_painting_removed() NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
{ _u8L("Custom supports and color painting were removed before repairing."));
get_notification_manager()->push_notification( }
NotificationType::CustomSupportsAndSeamRemovedAfterRepair,
NotificationManager::NotificationLevel::PrintInfoNotificationLevel,
_u8L("Custom supports and color painting were removed before repairing."));
} }
void Plater::changed_mesh(int obj_idx) void Plater::changed_mesh(int obj_idx)
-1
View File
@@ -599,7 +599,6 @@ public:
void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false); void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false);
void clear_before_change_mesh(int obj_idx); void clear_before_change_mesh(int obj_idx);
void notify_painting_removed();
void changed_mesh(int obj_idx); void changed_mesh(int obj_idx);
void changed_object(ModelObject &object); void changed_object(ModelObject &object);
+232
View File
@@ -0,0 +1,232 @@
#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
@@ -0,0 +1,36 @@
#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
-1
View File
@@ -3098,7 +3098,6 @@ void TabPrint::build()
optgroup = page->new_optgroup(L("Raft"), L"param_raft"); optgroup = page->new_optgroup(L("Raft"), L"param_raft");
optgroup->append_single_option_line("raft_layers", "support_settings_raft"); optgroup->append_single_option_line("raft_layers", "support_settings_raft");
optgroup->append_single_option_line("raft_contact_distance", "support_settings_raft"); optgroup->append_single_option_line("raft_contact_distance", "support_settings_raft");
optgroup->append_single_option_line("raft_expansion", "support_settings_raft");
optgroup = page->new_optgroup(L("Filament for Supports"), L"param_support_filament"); optgroup = page->new_optgroup(L("Filament for Supports"), L"param_support_filament");
optgroup->append_single_option_line("support_filament", "support_settings_filament#base"); optgroup->append_single_option_line("support_filament", "support_settings_filament#base");
+26 -17
View File
@@ -14,7 +14,7 @@
#include "Widgets/StateColor.hpp" #include "Widgets/StateColor.hpp"
#include "Widgets/StaticLine.hpp" #include "Widgets/StaticLine.hpp"
#include "libslic3r/ColorDecomposeRecipe.hpp" #include "libslic3r/ColorDecomposeRecipe.hpp"
#include "libslic3r/MeshRepair.hpp" #include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/TriangleSelector.hpp" #include "libslic3r/TriangleSelector.hpp"
#include <utility> #include <utility>
@@ -2436,25 +2436,25 @@ void TextureImportDialog::start_computation(bool auto_color, bool initial)
settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count; settings.target_colors_num = auto_color ? 0 : (size_t)m_param_color_count;
settings.smooth_weight = m_param_smooth / 10.0; settings.smooth_weight = m_param_smooth / 10.0;
settings.mesh_repair_decision = m_mesh_repair_decision; settings.mesh_repair_decision = m_mesh_repair_decision;
// BBS repairs the mesh through the Windows 3D SDK, which is only available on Windows
// builds that ship the SDK. Orca's CGAL-based repair (MeshBoolean::cgal::repair) works
// on all three platforms, so use that instead.
settings.mesh_repair_callback = [](const indexed_triangle_set& mesh, settings.mesh_repair_callback = [](const indexed_triangle_set& mesh,
indexed_triangle_set& repaired_mesh, indexed_triangle_set& repaired_mesh,
std::function<void(const char*, unsigned)> progress_callback, std::function<void(const char*, unsigned)> progress_callback,
std::function<bool()> cancel_callback, std::function<bool()> cancel_callback,
std::string* error_message) -> bool { std::string* error_message) -> bool {
if (cancel_callback && cancel_callback())
return false;
if (progress_callback) if (progress_callback)
progress_callback(_u8L("Repairing mesh").c_str(), 0); progress_callback(_u8L("Repairing mesh").c_str(), 0);
struct Canceled {};
TriangleMesh tm(mesh); TriangleMesh tm(mesh);
try { if (!MeshBoolean::cgal::repair(tm, nullptr, error_message))
if (!repair_mesh(tm, nullptr, error_message, [&cancel_callback](int) { return false;
if (cancel_callback && cancel_callback())
throw Canceled(); if (cancel_callback && cancel_callback())
}))
return false;
} catch (const Canceled &) {
return false; return false;
}
repaired_mesh = tm.its; repaired_mesh = tm.its;
if (progress_callback) if (progress_callback)
progress_callback(_u8L("Repairing mesh").c_str(), 100); progress_callback(_u8L("Repairing mesh").c_str(), 100);
@@ -2683,12 +2683,12 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
m_progress_dlg = nullptr; m_progress_dlg = nullptr;
} }
#ifdef HAS_WIN10SDK
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this, Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
_L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it before importing."), _L("The mesh has non-manifold geometry or open boundaries. You can import it as-is or repair it with Windows 3D repair service before importing."),
_L("Mesh repair"), wxYES_NO | wxCANCEL | wxICON_WARNING | wxYES_DEFAULT); _L("Mesh repair"), wxYES_NO | wxICON_WARNING | wxYES_DEFAULT);
dlg.SetButtonLabel(wxID_YES, _L("Import without repair")); dlg.SetButtonLabel(wxID_YES, _L("Import without repair"));
dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true); dlg.SetButtonLabel(wxID_NO, _L("Repair and import"), true);
dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel"));
// "Repair and import" is the recommended action here, so the accent moves off the default YES // "Repair and import" is the recommended action here, so the accent moves off the default YES
// button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling // button onto NO. MsgDialog::add_button already styled both as ButtonType::Choice, so restyling
// with the same type swaps only the palette and leaves the geometry alone. // with the same type swaps only the palette and leaves the geometry alone.
@@ -2704,7 +2704,17 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
dlg.Fit(); dlg.Fit();
dlg.CenterOnParent(); dlg.CenterOnParent();
int ret = dlg.ShowModal(); int ret = dlg.ShowModal();
if (ret == wxID_CANCEL) { m_mesh_repair_decision = (ret == wxID_NO)
? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport
: Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
#else
Slic3r::GUI::MessageDialog dlg(initial ? GetParent() : this,
_L("Please note that the mesh has non-manifold geometry or open boundaries."),
_L("Mesh issue"), wxOK | wxCANCEL | wxICON_WARNING | wxOK_DEFAULT);
dlg.SetButtonLabel(wxID_OK, _L("Continue"), true);
dlg.SetButtonLabel(wxID_CANCEL, _L("Cancel"));
int ret = dlg.ShowModal();
if (ret != wxID_OK) {
m_cancel_flag = true; m_cancel_flag = true;
if (initial) { if (initial) {
m_initial_computation_cancelled = true; m_initial_computation_cancelled = true;
@@ -2717,9 +2727,8 @@ void TextureImportDialog::on_mesh_repair_decision_required(wxCommandEvent&)
} }
return; return;
} }
m_mesh_repair_decision = (ret == wxID_NO) m_mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
? Slic3r::TexturePaintingSettings::MeshRepairDecision::RepairAndImport #endif
: Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
start_computation(auto_color, initial); start_computation(auto_color, initial);
} }
+30 -77
View File
@@ -27,7 +27,6 @@
#include <wx/mstream.h> #include <wx/mstream.h>
#include "libslic3r/PNGReadWrite.hpp" #include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "libslic3r/AppConfig.hpp" #include "libslic3r/AppConfig.hpp"
@@ -159,28 +158,6 @@ bool encode_png_bytes(const wxImage &image, std::vector<unsigned char> &out, std
: encode_gray_png_bytes(image, out, error); : encode_gray_png_bytes(image, out, error);
} }
// A wxImage over decoded pixels, so the encoders above can be reached from an in-memory image as well as
// from a file. Grayscale is expanded to the three equal channels wxImage always stores, which is what
// image_has_color() then reads as "no colour" - so a grey image still encodes as a grayscale PNG.
//
// `scratch` holds the pixel buffer: the wxImage is constructed over it without taking ownership, so it
// has to outlive every use of the returned image.
wxImage image_from_decoded(const DecodedHeightTexture &image, std::vector<unsigned char> &scratch)
{
if (image.empty())
return wxImage();
const size_t n = size_t(image.width) * size_t(image.height);
scratch.resize(n * 3);
if (image.has_color())
scratch.assign(image.rgb.begin(), image.rgb.end());
else
for (size_t i = 0; i < n; ++i)
scratch[i * 3] = scratch[i * 3 + 1] = scratch[i * 3 + 2] = image.pixels[i];
return wxImage(image.width, image.height, scratch.data(), /* static_data */ true);
}
std::vector<unsigned char> read_file_bytes(const std::string &path) std::vector<unsigned char> read_file_bytes(const std::string &path)
{ {
std::ifstream ifs(path, std::ios::binary); std::ifstream ifs(path, std::ios::binary);
@@ -209,42 +186,6 @@ bool is_supported_height_map(const std::vector<unsigned char> &bytes)
std::vector<TextureLibraryEntry> g_library; std::vector<TextureLibraryEntry> g_library;
bool g_library_scanned = false; bool g_library_scanned = false;
// Writes `bytes` into the user's own texture folder as `stem`.png, uniquified so an existing texture -
// shipped-name collision, or a second edit of the same source - is never overwritten. Returns the new
// file's library entry. Shared by import_texture_to_library() and save_texture_to_library(), which
// differ only in where their bytes came from.
std::optional<TextureLibraryEntry> write_user_texture(const std::vector<unsigned char> &bytes, const std::string &stem,
std::string &error)
{
const std::string user_dir = user_texture_dir();
if (user_dir.empty()) {
error = _u8L("Could not create the folder for imported textures.");
return std::nullopt;
}
boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png");
for (int i = 2; boost::filesystem::exists(dest); ++i)
dest = boost::filesystem::path(user_dir) / (stem + " (" + std::to_string(i) + ").png");
{
boost::nowide::ofstream ofs(dest.string(), std::ios::binary);
ofs.write(reinterpret_cast<const char *>(bytes.data()), std::streamsize(bytes.size()));
if (!ofs.good()) {
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
}
}
texture_library(true); // pick the new file up
const std::string dest_str = dest.string();
for (const TextureLibraryEntry &e : g_library)
if (e.path == dest_str)
return e;
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
}
// The user's own textures, most recently used first, kept in the app config. File names are stored rather // The user's own textures, most recently used first, kept in the app config. File names are stored rather
// than paths so the order survives a moved data directory; '/' separates them because no platform allows // than paths so the order survives a moved data directory; '/' separates them because no platform allows
// it inside a file name. Bounded so a long-lived config does not grow a line without end. // it inside a file name. Bounded so a long-lived config does not grow a line without end.
@@ -376,6 +317,12 @@ bool remove_user_texture(const std::string &path, std::string &error)
std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error) std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &source_path, std::string &error)
{ {
const std::string user_dir = user_texture_dir();
if (user_dir.empty()) {
error = _u8L("Could not create the folder for imported textures.");
return std::nullopt;
}
wxImage image; wxImage image;
if (!image.LoadFile(from_u8(source_path)) || !image.IsOk()) { if (!image.LoadFile(from_u8(source_path)) || !image.IsOk()) {
error = _u8L("Could not load the selected image."); error = _u8L("Could not load the selected image.");
@@ -386,7 +333,30 @@ std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &
if (!encode_png_bytes(image, bytes, error)) if (!encode_png_bytes(image, bytes, error))
return std::nullopt; return std::nullopt;
return write_user_texture(bytes, boost::filesystem::path(source_path).stem().string(), error); // Never overwrite an existing texture (the user's or, if they picked the same name twice, their
// own earlier import) - uniquify instead.
const std::string stem = boost::filesystem::path(source_path).stem().string();
boost::filesystem::path dest = boost::filesystem::path(user_dir) / (stem + ".png");
for (int i = 2; boost::filesystem::exists(dest); ++i)
dest = boost::filesystem::path(user_dir) / (stem + " (" + std::to_string(i) + ").png");
{
boost::nowide::ofstream ofs(dest.string(), std::ios::binary);
ofs.write(reinterpret_cast<const char *>(bytes.data()), std::streamsize(bytes.size()));
if (!ofs.good()) {
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
}
}
texture_library(true); // pick the new file up
const std::string dest_str = dest.string();
for (const TextureLibraryEntry &e : g_library)
if (e.path == dest_str)
return e;
error = _u8L("Failed to save the imported texture.");
return std::nullopt;
} }
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error) std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error)
@@ -412,21 +382,4 @@ std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::s
return std::make_shared<std::vector<unsigned char>>(std::move(converted)); return std::make_shared<std::vector<unsigned char>>(std::move(converted));
} }
std::optional<TextureLibraryEntry> save_texture_to_library(const DecodedHeightTexture &image, const std::string &stem,
std::string &error)
{
std::vector<unsigned char> scratch;
const wxImage wx = image_from_decoded(image, scratch);
if (!wx.IsOk()) {
error = _u8L("The selected image is empty.");
return std::nullopt;
}
std::vector<unsigned char> bytes;
if (!encode_png_bytes(wx, bytes, error))
return std::nullopt;
return write_user_texture(bytes, stem.empty() ? std::string("texture") : stem, error);
}
} // namespace Slic3r::GUI } // namespace Slic3r::GUI
-10
View File
@@ -6,8 +6,6 @@
#include <string> #include <string>
#include <vector> #include <vector>
#include "libslic3r/TextureDisplacement.hpp"
namespace Slic3r::GUI { namespace Slic3r::GUI {
// One selectable height-map texture in the texture-displacement gizmo's texture picker. // One selectable height-map texture in the texture-displacement gizmo's texture picker.
@@ -59,14 +57,6 @@ std::optional<TextureLibraryEntry> import_texture_to_library(const std::string &
// Returns nullptr with `error` set if the file cannot be read or decoded at all. // Returns nullptr with `error` set if the file cannot be read or decoded at all.
std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error); std::shared_ptr<std::vector<unsigned char>> load_texture_image_data(const std::string &path, std::string &error);
// Writes `image` into user_texture_dir() as a new PNG named after `stem` (uniquified if taken, so an
// existing texture is never overwritten) and returns its library entry. The in-memory counterpart of
// import_texture_to_library(), for an image that was edited rather than read off disk.
//
// Returns nullopt with `error` set on failure.
std::optional<TextureLibraryEntry> save_texture_to_library(const DecodedHeightTexture &image, const std::string &stem,
std::string &error);
} // namespace Slic3r::GUI } // namespace Slic3r::GUI
#endif // slic3r_TextureLibrary_hpp_ #endif // slic3r_TextureLibrary_hpp_
+2 -23
View File
@@ -36,7 +36,6 @@
#include "3DScene.hpp" #include "3DScene.hpp"
#include "BitmapCache.hpp" #include "BitmapCache.hpp"
#include "GLCanvas3D.hpp"
#include "GLShader.hpp" #include "GLShader.hpp"
#include "GUI_App.hpp" #include "GUI_App.hpp"
#include "I18N.hpp" #include "I18N.hpp"
@@ -940,8 +939,7 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair<int, int>{ -1, -1 }; m_active_edge = (hit.first >= 0 && is_edge_selected(hit)) ? hit : std::pair<int, int>{ -1, -1 };
m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan; m_gesture = (m_active_edge.first >= 0) ? Gesture::MoveEdge : Gesture::Pan;
} else { } else {
const std::vector<int> selection_before = m_selection; const int hit = island_at(uv);
const int hit = island_at(uv);
if (hit >= 0) { if (hit >= 0) {
if (evt.ShiftDown()) { if (evt.ShiftDown()) {
// Shift adds to the selection (and makes the clicked one the new primary). // Shift adds to the selection (and makes the clicked one the new primary).
@@ -971,10 +969,6 @@ void UVEditorCanvas::on_mouse(wxMouseEvent &evt)
m_selected_island = -1; m_selected_island = -1;
} }
m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan; m_gesture = (m_selected_island >= 0) ? Gesture::MoveIsland : Gesture::Pan;
// The owner shows the selection on the model and would not hear of this click otherwise in time: a
// click that clears the selection starts no gesture, and one that sets it reports only on release.
if (m_selection != selection_before)
run_command(Command::SelectionChanged);
} }
grab_mouse(); grab_mouse();
Refresh(); Refresh();
@@ -1325,13 +1319,8 @@ void UVEditorCanvas::on_size(wxSizeEvent &evt)
// Update() flushes it immediately - still through the normal paint path (unlike calling // Update() flushes it immediately - still through the normal paint path (unlike calling
// render() directly), which matters because this canvas shares the app's one GL context with // render() directly), which matters because this canvas shares the app's one GL context with
// the 3D view and must not hijack it outside its own paint. // the 3D view and must not hijack it outside its own paint.
// Not while a 3D canvas frame is in progress, though: wxGTK delivers this size event synchronously
// when the texture displacement gizmo relays out the pane from its ImGui panel, i.e. from inside
// that frame, and flushing the paint there would take the shared context away from it. The Refresh()
// then runs the paint once the frame is over.
Refresh(); Refresh();
if (!GLCanvas3D::is_frame_in_progress()) Update();
Update();
} }
void UVEditorCanvas::on_leave(wxMouseEvent &evt) void UVEditorCanvas::on_leave(wxMouseEvent &evt)
@@ -1352,16 +1341,6 @@ void UVEditorCanvas::render()
{ {
if (m_context == nullptr || !IsShownOnScreen()) if (m_context == nullptr || !IsShownOnScreen())
return; return;
// Painted from inside a 3D canvas frame - a modal dialog the gizmo opened from its ImGui panel runs
// this paint, as does any other synchronous event raised there. Making this surface current now
// would take the shared context away from that canvas mid-frame: its remaining draws would land
// here and, on Mesa/X11, its SwapBuffers() would be a fatal BadMatch. Skip, but ask for another
// paint so the skipped one is not simply lost. The pane stays stale for as long as a nested modal
// loop keeps a frame on the stack, which is behind the dialog anyway.
if (GLCanvas3D::is_frame_in_progress()) {
Refresh();
return;
}
// wxGLCanvas::SetCurrent() genuinely fails (returns false) on a canvas that isn't shown on // wxGLCanvas::SetCurrent() genuinely fails (returns false) on a canvas that isn't shown on
// screen, and can also fail on a pixel-format mismatch with the context - and every GL call // screen, and can also fail on a pixel-format mismatch with the context - and every GL call
+1 -3
View File
@@ -108,12 +108,10 @@ public:
// the command callback - the canvas has the selection and the view, the gizmo has the layer. // the command callback - the canvas has the selection and the view, the gizmo has the layer.
// `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into // `value` carries the new setting for the Set* commands (an angle, a flag as 0/1, or an index into
// SelectMode / Background) and is 0 otherwise. // SelectMode / Background) and is 0 otherwise.
// SelectionChanged is sent by the canvas itself when a click changes the island selection. It carries
// nothing: the owner reads selected_islands() back, and only needs telling that it should look.
enum class Command { enum class Command {
FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected, FrameAll, ToggleSnap, AverageScale, CutSelectedIsland, ProjectFromView, JoinSelected, UnjoinSelected,
Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits, Unwrap, SetSeamAngle, SetConnectIslands, SetSelectMode, SetMarkSeams, SetSeamPath, ClearSeams, ClearUVEdits,
SetBackground, PickTexture, PaneClosed, SelectionChanged SetBackground, PickTexture, PaneClosed
}; };
void run_command(Command cmd, float value = 0.f); void run_command(Command cmd, float value = 0.f);
using CommandFn = std::function<void(Command, float)>; using CommandFn = std::function<void(Command, float)>;
View File
View File
+9 -29
View File
@@ -82,11 +82,6 @@ ComboBox::ComboBox(wxWindow *parent,
e.SetId(GetId()); e.SetId(GetId());
GetEventHandler()->ProcessEvent(e); 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.Bind(EVT_DISMISS, [this](auto &) {
drop_down = false; drop_down = false;
wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP); wxCommandEvent e(wxEVT_COMBOBOX_CLOSEUP);
@@ -256,7 +251,7 @@ int ComboBox::Append(const wxString &text,
int style) int style)
{ {
auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap; auto valid_bit_map = bitmap.IsOk() ? bitmap : wxNullBitmap;
Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, wxNullBitmap, clientData, group_key, group_label}; Item item{text, wxEmptyString, valid_bit_map, valid_bit_map, clientData, group_key, group_label};
item.style = style; item.style = style;
items.push_back(item); items.push_back(item);
SetClientDataType(wxClientData_Void); SetClientDataType(wxClientData_Void);
@@ -302,12 +297,6 @@ void ComboBox::ForceDropdownOpen()
} }
} }
void ComboBox::CloseDropdown()
{
if (drop.IsShown())
drop.DismissAndNotify();
}
unsigned int ComboBox::GetCount() const { return items.size(); } unsigned int ComboBox::GetCount() const { return items.size(); }
void ComboBox::set_replace_text(wxString text, wxString image_name) void ComboBox::set_replace_text(wxString text, wxString image_name)
@@ -361,13 +350,6 @@ void ComboBox::SetItemBitmap(unsigned int n, wxBitmap const &bitmap)
drop.Invalidate(); 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, int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
unsigned int pos, unsigned int pos,
void ** clientData, void ** clientData,
@@ -375,7 +357,7 @@ int ComboBox::DoInsertItems(const wxArrayStringsAdapter &items,
{ {
if (pos > this->items.size()) return -1; if (pos > this->items.size()) return -1;
for (int i = 0; i < items.GetCount(); ++i) { for (int i = 0; i < items.GetCount(); ++i) {
Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL }; Item item { items[i], wxEmptyString, wxNullBitmap, wxNullBitmap, clientData ? clientData[i] : NULL };
this->items.insert(this->items.begin() + pos, item); this->items.insert(this->items.begin() + pos, item);
++pos; ++pos;
} }
@@ -437,18 +419,16 @@ void ComboBox::keyDown(wxKeyEvent& event)
case WXK_UP: case WXK_UP:
case WXK_DOWN: case WXK_DOWN:
case WXK_LEFT: case WXK_LEFT:
case WXK_RIGHT: { case WXK_RIGHT:
// Step over disabled rows: the mouse cannot select them either. if ((event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) && GetSelection() > 0) {
const int step = (event.GetKeyCode() == WXK_UP || event.GetKeyCode() == WXK_LEFT) ? -1 : 1; SetSelection(GetSelection() - 1);
int n = GetSelection() + step; } else if ((event.GetKeyCode() == WXK_DOWN || event.GetKeyCode() == WXK_RIGHT) && GetSelection() + 1 < items.size()) {
while (n >= 0 && n < int(items.size()) && (items[n].style & DD_ITEM_STYLE_DISABLED)) SetSelection(GetSelection() + 1);
n += step; } else {
if (n < 0 || n >= int(items.size()))
break; break;
SetSelection(n); }
sendComboBoxEvent(); sendComboBoxEvent();
break; break;
}
case WXK_TAB: case WXK_TAB:
HandleAsNavigationKey(event); HandleAsNavigationKey(event);
break; break;
-2
View File
@@ -96,12 +96,10 @@ public:
wxBitmap GetItemBitmap(unsigned int n); wxBitmap GetItemBitmap(unsigned int n);
void SetItemBitmap(unsigned int n, wxBitmap const &bitmap); void SetItemBitmap(unsigned int n, wxBitmap const &bitmap);
void SetItemActionBitmap(unsigned int n, wxBitmap const &bitmap);
bool is_drop_down(){return drop_down;} bool is_drop_down(){return drop_down;}
void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); } void DeleteOneItem(unsigned int pos) { DoDeleteOneItem(pos); }
void ForceDropdownOpen(); void ForceDropdownOpen();
void CloseDropdown();
protected: protected:
virtual int DoInsertItems(const wxArrayStringsAdapter &items, virtual int DoInsertItems(const wxArrayStringsAdapter &items,
+4 -64
View File
@@ -30,7 +30,6 @@
#include "slic3r/GUI/wxExtensions.hpp" #include "slic3r/GUI/wxExtensions.hpp"
wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent); wxDEFINE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDEFINE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
BEGIN_EVENT_TABLE(DropDown, PopupWindow) BEGIN_EVENT_TABLE(DropDown, PopupWindow)
@@ -243,19 +242,6 @@ static wxSize GetBmpSize(wxBitmap & bmp)
#endif #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) static void _DrawSplitItem(const wxWindow* w, wxDC& dc, wxString split_text, wxPoint start_pt, int item_width, int item_height)
{ {
// save dc // save dc
@@ -385,10 +371,6 @@ void DropDown::render(wxDC &dc)
rcContent.width -= szBmp.x + 5; 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; std::set<wxString> groups;
// draw texts & icons // draw texts & icons
int index = 0; int index = 0;
@@ -452,24 +434,13 @@ void DropDown::render(wxDC &dc)
auto text = group.IsEmpty() auto text = group.IsEmpty()
? (item.group_key.IsEmpty() ? item.text : item.group_label) ? (item.group_key.IsEmpty() ? item.text : item.group_label)
: item.text; : 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()) { if (!text_off && !text.IsEmpty()) {
wxSize tSize = dc.GetMultiLineTextExtent(text); wxSize tSize = dc.GetMultiLineTextExtent(text);
if (pt.x + tSize.x > rcContent.GetRight()) { if (pt.x + tSize.x > rcContent.GetRight()) {
int max_text_width = rcContent.GetRight() - pt.x; if (is_hover && item.tip.IsEmpty())
if (max_text_width <= 0) { SetToolTip(text);
text.clear(); text = wxControl::Ellipsize(text, dc, wxELLIPSIZE_END,
} else { rcContent.GetRight() - pt.x);
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; pt.y += (rcContent.height - textSize.y) / 2;
dc.SetFont(GetFont()); dc.SetFont(GetFont());
@@ -558,7 +529,6 @@ void DropDown::messureSize()
if (!need_sync) return; if (!need_sync) return;
textSize = wxSize(); textSize = wxSize();
iconSize = wxSize(); iconSize = wxSize();
actionIconWidth = 0;
count = 0; count = 0;
wxClientDC dc(GetParent() ? GetParent() : this); wxClientDC dc(GetParent() ? GetParent() : this);
dc.SetFont(GetFont()); dc.SetFont(GetFont());
@@ -595,13 +565,8 @@ void DropDown::messureSize()
size1.x += size2.x + (text_off ? 0 : 5); 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; 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; if (!align_icon) iconSize.x = 0;
wxSize szContent = textSize; wxSize szContent = textSize;
if (szContent.x < FromDIP(120) && !use_content_width) if (szContent.x < FromDIP(120) && !use_content_width)
@@ -671,11 +636,6 @@ void DropDown::messureSize()
e.SetId(GetId()); e.SetId(GetId());
GetEventHandler()->ProcessEvent(e); GetEventHandler()->ProcessEvent(e);
}); });
subDropDown->Bind(EVT_DROPDOWN_ITEM_ACTION, [this](wxCommandEvent &e) {
e.SetEventObject(this);
e.SetId(GetId());
GetEventHandler()->ProcessEvent(e);
});
} }
need_sync = false; need_sync = false;
} }
@@ -754,15 +714,6 @@ void DropDown::mouseReleased(wxMouseEvent& event)
if (hover_item < 0) if (hover_item < 0)
return; return;
if (hover_item >= 0 && (subDropDown == nullptr || subDropDown->group.empty())) { // not moved 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(); sendDropDownEvent();
if (mainDropDown) if (mainDropDown)
mainDropDown->hover_item = -1; // To Dismiss mainDropDown mainDropDown->hover_item = -1; // To Dismiss mainDropDown
@@ -879,17 +830,6 @@ void DropDown::sendDropDownEvent()
GetEventHandler()->ProcessEvent(event); 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() void DropDown::Dismiss()
{ {
if (subDropDown && subDropDown->IsShown()) if (subDropDown && subDropDown->IsShown())
-6
View File
@@ -5,7 +5,6 @@
#include <wx/event.h> #include <wx/event.h>
#include <vector> #include <vector>
#include <cstddef> #include <cstddef>
#include <utility>
#include <wx/gdicmn.h> #include <wx/gdicmn.h>
#include "slic3r/GUI/Widgets/StateColor.hpp" #include "slic3r/GUI/Widgets/StateColor.hpp"
#include <boost/date_time/posix_time/ptime.hpp> #include <boost/date_time/posix_time/ptime.hpp>
@@ -28,7 +27,6 @@ class wxWindow;
#define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable #define DD_ITEM_STYLE_DIMMED 0x0004 // gray text, but still selectable
wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent); wxDECLARE_EVENT(EVT_DISMISS, wxCommandEvent);
wxDECLARE_EVENT(EVT_DROPDOWN_ITEM_ACTION, wxCommandEvent);
class DropDown : public PopupWindow class DropDown : public PopupWindow
{ {
@@ -39,7 +37,6 @@ public:
wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox wxString text_static_tips;// display static tips for TextInput.eg.PrinterInfoBox
wxBitmap icon; wxBitmap icon;
wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox wxBitmap icon_textctrl;// display icon for TextInput.eg.PrinterInfoBox
wxBitmap action_icon;
void * data{nullptr}; void * data{nullptr};
wxString group_key{}; wxString group_key{};
wxString group_label{}; wxString group_label{};
@@ -68,7 +65,6 @@ private:
wxSize textSize; wxSize textSize;
wxSize iconSize; wxSize iconSize;
int actionIconWidth = 0;
wxSize rowSize; wxSize rowSize;
StateHandler state_handler; StateHandler state_handler;
@@ -139,7 +135,6 @@ private:
int hoverIndex(); int hoverIndex();
int selectedItem(); int selectedItem();
std::pair<int, int> actionColumn() const;
friend class ComboBox; friend class ComboBox;
void messureSize(); void messureSize();
@@ -153,7 +148,6 @@ private:
void mouseWheelMoved(wxMouseEvent &event); void mouseWheelMoved(wxMouseEvent &event);
void sendDropDownEvent(); void sendDropDownEvent();
void sendDropDownActionEvent(int index);
DECLARE_EVENT_TABLE() DECLARE_EVENT_TABLE()
+27
View File
@@ -69,6 +69,10 @@ webkit_web_view_run_javascript_finish (WebKitWebView
GError **error); GError **error);
WEBKIT_API void WEBKIT_API void
webkit_javascript_result_unref (WebKitJavascriptResult *js_result); webkit_javascript_result_unref (WebKitJavascriptResult *js_result);
WEBKIT_API guint
webkit_get_major_version (void);
WEBKIT_API guint
webkit_get_minor_version (void);
} }
#endif #endif
@@ -401,6 +405,29 @@ bool WebView::NeedsRecreateOnShow()
return false; return false;
#endif #endif
} }
bool WebView::NeedsCompositingWorkaround()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major < 2 || (major == 2 && minor < 42);
#else
return false;
#endif
}
bool WebView::SupportsForceSharedMemory()
{
#ifdef __linux__
const guint major = webkit_get_major_version();
const guint minor = webkit_get_minor_version();
return major > 2 || (major == 2 && minor >= 44);
#else
return false;
#endif
}
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
bool WebView::CheckWebViewRuntime() bool WebView::CheckWebViewRuntime()
{ {
+10
View File
@@ -30,6 +30,16 @@ public:
// ignoring every navigation. A panel that gets true here recreates its view on first Show(). // ignoring every navigation. A panel that gets true here recreates its view on first Show().
static bool NeedsRecreateOnShow(); static bool NeedsRecreateOnShow();
// True when the linked WebKitGTK predates 2.42, where accelerated compositing
// could fail silently on XWayland sessions, so it is forced off as a fallback.
// False on every other platform. Safe to call before GTK initialises.
static bool NeedsCompositingWorkaround();
// True when the linked WebKitGTK is new enough (>= 2.44) to honour
// WEBKIT_DMABUF_RENDERER_FORCE_SHM, the shared-memory transport switch.
// False on every other platform. Safe to call before GTK initialises.
static bool SupportsForceSharedMemory();
static void RecreateAll(); static void RecreateAll();
}; };
+15
View File
@@ -0,0 +1,15 @@
#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
+183 -58
View File
@@ -3,115 +3,240 @@
#include <algorithm> #include <algorithm>
#include <atomic> #include <atomic>
#include <chrono> #include <chrono>
#include <cmath>
#include <condition_variable> #include <condition_variable>
#include <cstddef> #include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/libslic3r.h"
#include <exception> #include <exception>
#include <cstddef>
#include "libslic3r/Exception.hpp"
#include <limits>
#include <mutex> #include <mutex>
#include "slic3r/GUI/Widgets/ProgressDialog.hpp"
#include <optional>
#include <string> #include <string>
#include <thread> #include <thread>
#include <utility>
#include <vector> #include <vector>
#include <wx/string.h> #include <wx/string.h>
#include "libslic3r/Format/bbs_3mf.hpp" #include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/MeshRepair.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/Format/bbs_3mf.hpp"
#include "libslic3r/format.hpp"
#include "libslic3r/Thread.hpp" #include "libslic3r/Thread.hpp"
#include "slic3r/GUI/I18N.hpp" #include "../GUI/I18N.hpp"
#include "slic3r/GUI/Widgets/ProgressDialog.hpp" #include "libslic3r/TriangleSelector.hpp"
// Orca: This file provides utilities for repairing 3D model meshes using the CGAL library, handling mesh splitting, merging, and boolean operations.
namespace Slic3r { namespace Slic3r {
namespace { namespace {
class RepairCanceledException : public std::exception // Orca: Helper functions for analyzing mesh properties and transformations.
bool is_not_3dimensional_part(const TriangleMesh &mesh)
{ {
public: // Orca: Determines if a mesh is degenerate or represents a non-3dimensional part by checking volume and bounding box dimensions.
const char *what() const noexcept override { return "Model repair has been canceled"; } if (mesh.its.indices.empty())
}; return true;
indexed_triangle_set tmp = mesh.its;
its_remove_degenerate_faces(tmp, true);
if (tmp.indices.empty())
return true;
const BoundingBoxf3 bbox = mesh.bounding_box();
const Vec3d size = bbox.size();
const double min_dim = std::min(size.x(), std::min(size.y(), size.z()));
const double max_dim = std::max(size.x(), std::max(size.y(), size.z()));
if (min_dim <= EPSILON)
return true;
const double volume = std::abs(its_volume(mesh.its));
const double bbox_volume = size.x() * size.y() * size.z();
if (volume <= EPSILON)
return true;
const double min_relative_thickness = 1e-6;
const double min_volume_ratio = 1e-6;
if (min_dim / max_dim <= min_relative_thickness)
return true;
if (bbox_volume > 0.0 && volume / bbox_volume <= min_volume_ratio)
return true;
return false;
}
} // namespace } // namespace
bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting, // Orca: Exception class for handling user-initiated cancellation of model repair operations.
bool *modified) class RepairCanceledException : public std::exception {
public:
const char* what() const noexcept override { return "Model repair has been canceled"; }
};
// Orca: Main function to repair model objects using CGAL, with progress dialog and cancellation support.
// Returns false if fixing was canceled. fix_result contains error message if failed.
bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dialog, const wxString &msg_header, std::string &fix_result, bool keep_painting)
{ {
// Keeps the backup manager from reading a mesh while the worker replaces it. // Hold SaveObjectGaurd to prevent backup manager from racing concurrent mesh mutations (use-after-free).
SaveObjectGaurd backup_gaurd(model_object); SaveObjectGaurd backup_gaurd(model_object);
std::vector<ModelVolume *> volumes; // Orca: Synchronization primitives for progress updates between worker thread and GUI.
if (volume_idx < 0) std::mutex mtx;
volumes = model_object.volumes;
else if (std::size_t(volume_idx) < model_object.volumes.size())
volumes.push_back(model_object.volumes[volume_idx]);
std::mutex mtx;
std::condition_variable condition; std::condition_variable condition;
struct Progress struct Progress {
{
std::string message; std::string message;
int percent = 0; int percent = 0;
bool updated = false; bool updated = false;
} progress; } progress;
std::atomic<bool> canceled = false; std::atomic<bool> canceled = false;
std::atomic<bool> finished = false; std::atomic<bool> finished = false;
bool any_modified = false;
std::string errors;
auto on_progress = [&mtx, &condition, &progress](const std::string &msg, int percent) { bool success = false;
std::lock_guard<std::mutex> lock(mtx); size_t ivolume = 0;
// Orca: Lambda for updating progress from worker thread.
auto on_progress = [&mtx, &condition, &ivolume, &model_object, &progress](const std::string &msg, unsigned prcnt) {
std::unique_lock<std::mutex> lock(mtx);
progress.message = msg; progress.message = msg;
progress.percent = percent; const size_t total = std::max<size_t>(1, model_object.volumes.size());
progress.percent = int(std::floor((float(prcnt) + float(ivolume) * 100.f) / float(total)));
progress.updated = true; progress.updated = true;
condition.notify_all(); condition.notify_all();
}; };
auto worker_thread = std::thread([&]() { // Orca: Worker thread that performs the actual model repair operations.
set_current_thread_name("cgal_fix_model"); auto worker_thread = std::thread([&model_object, volume_idx, &ivolume, on_progress, &success, &canceled, &finished, &fix_result, keep_painting]() {
try { try {
for (std::size_t i = 0; i < volumes.size(); ++i) { set_current_thread_name("cgal_fix_model");
ModelVolume *volume = volumes[i];
const std::string name = volumes.size() > 1 ? volume->name : std::string();
const auto report = [&](int percent) {
if (canceled)
throw RepairCanceledException();
on_progress(name, int((100 * i + std::size_t(percent)) / volumes.size()));
};
report(0);
MeshRepairStats stats; size_t start_volume = volume_idx == -1 ? 0 : size_t(volume_idx);
std::string error; size_t end_volume = volume_idx == -1 ? std::numeric_limits<size_t>::max() : size_t(volume_idx);
if (!volume->repair(keep_painting, &stats, &error, report))
errors += (errors.empty() ? "" : "\n") + (volumes.size() > 1 ? volume->name + ": " : std::string()) + error; for (ivolume = start_volume; ivolume < model_object.volumes.size(); ++ivolume) {
any_modified = any_modified || stats.modified; if (volume_idx != -1 && ivolume > end_volume)
break;
if (canceled)
throw RepairCanceledException();
on_progress(_u8L("Repairing model object"), 10);
ModelVolume *volume = model_object.volumes[ivolume];
// Orca: Split splittable volumes into parts for individual processing.
size_t parts_count = 1;
if (volume->is_splittable()) {
parts_count = volume->split(1, keep_painting);
if (parts_count > 1) {
const std::string msg = Slic3r::format(L("Split into %1% parts"), parts_count);
on_progress(msg, 10);
}
}
size_t part_end = std::min(ivolume + parts_count - 1, model_object.volumes.size() - 1);
if (volume_idx != -1)
end_volume = part_end;
size_t removed_parts = 0;
for (size_t idx = part_end + 1; idx > ivolume; --idx) {
const size_t part_idx = idx - 1;
const ModelVolume *part_volume = model_object.volumes[part_idx];
if (!is_not_3dimensional_part(part_volume->mesh()))
continue;
model_object.delete_volume(part_idx);
++removed_parts;
if (part_end > 0)
--part_end;
else
part_end = 0;
if (volume_idx != -1)
end_volume = part_end;
}
if (removed_parts >= parts_count) {
ivolume = part_end;
on_progress(_u8L("Repair finished"), 100);
continue;
}
for (size_t part_idx = ivolume; part_idx <= part_end && part_idx < model_object.volumes.size(); ++part_idx) {
ModelVolume *part_volume = model_object.volumes[part_idx];
TriangleMesh mesh = part_volume->mesh();
if (its_num_open_edges(mesh.its) != 0) {
// Save painting for later remap
const std::optional<TriangleSelector::SavedPainting> saved_painting = keep_painting ?
part_volume->save_painting() :
std::optional<TriangleSelector::SavedPainting>{};
std::string error;
if (!MeshBoolean::cgal::repair(mesh, nullptr, &error))
throw Slic3r::RuntimeError(error.empty() ? _u8L("Repair failed") : error);
part_volume->set_mesh(std::move(mesh));
part_volume->calculate_convex_hull();
part_volume->invalidate_convex_hull_2d();
part_volume->set_new_unique_id();
// Remap paint back
part_volume->restore_painting(saved_painting);
}
}
ivolume = part_end;
on_progress(_u8L("Repair finished"), 100);
} }
} catch (const RepairCanceledException &) {
model_object.invalidate_bounding_box();
if (ivolume > 0)
--ivolume;
on_progress(_u8L("Repair finished"), 100);
success = true;
finished = true;
} catch (RepairCanceledException &) {
canceled = true; canceled = true;
} catch (const std::exception &ex) { finished = true;
errors += (errors.empty() ? "" : "\n") + std::string(ex.what()); on_progress(_u8L("Repair canceled"), 100);
} catch (std::exception &ex) {
success = false;
finished = true;
fix_result = ex.what();
on_progress(ex.what(), 100);
} }
// Volumes repaired before a cancel or an error keep their new mesh.
model_object.invalidate_bounding_box();
finished = true;
on_progress(_u8L("Repair finished"), 100);
}); });
// Orca: Main GUI loop to update progress dialog and handle cancellation.
while (!finished) { while (!finished) {
std::unique_lock<std::mutex> lock(mtx); std::unique_lock<std::mutex> lock(mtx);
condition.wait_for(lock, std::chrono::milliseconds(250), [&progress] { return progress.updated; }); condition.wait_for(lock, std::chrono::milliseconds(250), [&progress]{ return progress.updated; });
// One percent short of the maximum, so that the dialog does not close itself.
if (!progress_dialog.Update(std::max(0, progress.percent - 1), msg_header + wxString::FromUTF8(progress.message))) // Decrease progress percent slightly to avoid auto-closing.
if (!progress_dialog.Update(progress.percent - 1, msg_header + _(progress.message)))
canceled = true; canceled = true;
else else
progress_dialog.Fit(); progress_dialog.Fit();
progress.updated = false; progress.updated = false;
} }
if (canceled) {
// Nothing to show.
} else if (success) {
fix_result.clear();
}
if (worker_thread.joinable()) if (worker_thread.joinable())
worker_thread.join(); worker_thread.join();
fix_result = canceled ? std::string() : errors;
if (modified)
*modified = any_modified;
return !canceled; return !canceled;
} }
+2 -3
View File
@@ -12,9 +12,8 @@ class Model;
class ModelObject; class ModelObject;
class Print; class Print;
// Return false if fixing was canceled. fix_result is empty on success; modified tells whether any mesh changed. // Return false if fixing was canceled. fix_result is empty on success.
extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting, extern bool fix_model_with_cgal_gui(ModelObject &model_object, int volume_idx, GUI::ProgressDialog &progress_dlg, const wxString &msg_header, std::string &fix_result, bool keep_painting);
bool *modified = nullptr);
} // namespace Slic3r } // namespace Slic3r
+19
View File
@@ -0,0 +1,19 @@
#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_
-16
View File
@@ -31,22 +31,6 @@ set_tests_properties(cli_project_missing_keys PROPERTIES
SKIP_RETURN_CODE 77 SKIP_RETURN_CODE 77
TIMEOUT 900) TIMEOUT 900)
add_test(NAME cli_arrange_rotations
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_arrange_rotations.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}
${CMAKE_SOURCE_DIR}/resources/profiles/BBL)
set_tests_properties(cli_arrange_rotations PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
add_test(NAME cli_no_suitable_objects
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_no_suitable_objects.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}
${CMAKE_SOURCE_DIR}/resources/profiles/BBL)
set_tests_properties(cli_no_suitable_objects PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
add_test(NAME cli_malformed_input add_test(NAME cli_malformed_input
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}) COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_malformed_input.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON})
set_tests_properties(cli_malformed_input PROPERTIES set_tests_properties(cli_malformed_input PROPERTIES

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