Compare commits

..
Author SHA1 Message Date
Kenneth Rapleeandyw4z 4964f49765 Fix size_t/%d format mismatch in MsgDialog::add_button (#15852)
Fix size_t/%d mismatch in MsgDialog button keys

`m_buttons.size()` is a `size_t`, which does not match the `%d` conversion in
printf-style variadics. Build the key with `std::to_string` instead; exact
for any size_t, no behavior change for realistic counts.

Co-authored-by: yw4z <ywsyildiz@gmail.com>
2026-09-27 19:21:03 +03:00
Chris Bennight ea471dc82d Fix model initialization without a wx application (#15866)
Guard the smooth-normals preference read when CLI thumbnail generation initializes geometry without a wx application. Use the existing flat-normal path in that case and preserve GUI preferences.
2026-09-27 18:53:11 +03:00
Kiss Lorand bd65cce3f4 Unify GUI scroll rates (#15902)
Use DPI-aware 20 DIP vertical scrolling for general-purpose GUI
scroll areas.

Keep list-based views aligned to their item height so one wheel increment follows the visible row rhythm.

This makes Preferences and other dialogs scroll consistently across
Windows DPI settings.
2026-09-27 16:58:10 +03:00
d976d9eb0c Release the DC after GetDC in get_dpi_for_window and font enumeration (#15919)
get_dpi_for_window's pre-8.1 fallback and get_font_list_by_enumeration
both called GetDC without a matching ReleaseDC, leaking a GDI handle
each call. get_dpi_for_window runs on every mouse-move over the 3D
viewport, so the leak exhausts the per-process GDI handle limit and
hangs the app within minutes on Windows 7/8.

Co-authored-by: Fernando Marino <f.marino@rheagroup.com>
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-27 14:19:27 +03:00
Ian Chua ae27a795b6 fix: timestamp format for OFL OTA update endpoint (#15936)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

The OFL OTA workflow sent an ISO-8601 timestamp to the pending-clear
endpoint, which only accepts Unix epoch seconds. This caused the
scheduled workflow to fail with HTTP 400.

Use `data -u +%s` so that request matches the endpoint contract. 

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-27 16:40:04 +08:00
peachismomo ecd0be353c fix: timestamp format for OFL OTA update endpoint 2026-09-27 16:36:15 +08:00
Kiss Lorand 6a07853933 Fix crash with placeholders in file header G-code (#15915) 2026-09-26 17:09:56 -03:00
Noisyfox 5ae9015c82 Fix rare crash due to invalid pointer when vector::resize reallocate underlying memory (#15877) 2026-09-26 16:52:29 -03:00
0ddc730854 No fuzzy skin on bridge like overhangs perimeters (#13891)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
2026-09-26 16:42:44 -03:00
Ian Bassi ea280ba6f6 Wipe tower sparse layers combination (#15841) 2026-09-26 16:27:15 -03:00
Kris Austin 237cd10eb5 fix: Home start shows Prepare or a blank window, and Prepare opens slowly (#15878) 2026-09-26 14:23:52 -03:00
Ioannis Giannakas e77d179bbe Fix inner-outer-inner wall ordering falling back to outer-inner on narrow walls with Arachne (#15924)
* Fix wall ordering edge case
* IOI performance tuning - greedy stop when a first touch is identified.
2026-09-26 17:44:52 +01:00
Ioannis Giannakas d5aaa463c8 Fix MacOS 27 Xcode and Command Line build failures (#15923) 2026-09-26 13:20:17 -03:00
Ian BassiandRodrigo Faselli 8c03985818 Add Cubic Non-crossing multiline strategy (#15887)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-09-26 11:40:49 -03:00
Kris Austin 93b58a2034 fix(gui): keyboard shortcuts cleanup after #15706 (#15862) 2026-09-25 21:51:19 -03:00
weng haishi 6be6fdd7c7 feat: add timestamp to ofl update workflow so that concurrent post_merge_profiles are not silently dropped (#15898)
# Description

<!--
> Please provide a summary of the changes made in this PR. Include
details such as:
  > * What issue does this PR address or fix?
  > * What new features or enhancements does this PR introduce?
> * Are there any breaking changes or dependencies that need to be
considered?
-->

If a vendor runs `/bot merge` while the cronjob is running, it might be
dropped because the table might be cleared before `post_merge_profiles`
completes. Instead we can add a timestamp so that we don't accidentally
drop any PR merges that occur while the OFL cronjob is running.

# Screenshots/Recordings/Graphs

<!--
> Please attach relevant screenshots to showcase the UI changes.
> Please attach images that can help explain the changes.
-->

## Tests

<!--
> Please describe the tests that you have conducted to verify the
changes made in this PR.
-->

<!--
> A guide for users on how to download the artifacts from this PR.
-->

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-25 17:53:22 +08:00
Ian Chua 521a30a45c feat: add timestamp to ofl update workflow so that concurrent post_merge_profiles are not silently dropped 2026-09-25 16:53:45 +08:00
98 changed files with 2776 additions and 1098 deletions
+55 -62
View File
@@ -12,9 +12,9 @@ name: Daily OFL OTA Update
# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after # vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after
# uploading - see post_merge_profiles.yml for both sides of that contract. # uploading - see post_merge_profiles.yml for both sides of that contract.
# #
# If at least one branch was dispatched this run, a final step clears OFL's pending-publish # At the start of each run, the pending-publish table is cleared up to a captured
# table (POST /api/v1/ota/ofl/pending/clear) - the daily "published everything, reset" signal. # timestamp (POST /api/v1/ota/ofl/pending/clear?timestamp=...). Changes merged after
# That table is populated only by this pipeline's own auto-publish calls. # that timestamp remain pending for the next run.
on: on:
schedule: schedule:
@@ -34,6 +34,32 @@ jobs:
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }} if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Capture start timestamp and clear OFL pending queue
id: start
shell: bash
env:
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
run: |
set -euo pipefail
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
timestamp="$(date -u +%s)"
echo "timestamp=$timestamp" >> "$GITHUB_OUTPUT"
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear?timestamp=$timestamp" \
-H "Authorization: Bearer $OTA_API_KEY")"
body="$(cat "$resp_file")"
echo "$body"
if [ "$status" != "200" ]; then
echo "::error::OTA pending-clear call failed with HTTP $status"
exit 1
fi
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v7 uses: actions/checkout@v7
with: with:
@@ -46,13 +72,12 @@ jobs:
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*' run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'
- name: Scan branches and publish changed OFL profiles - name: Scan branches and publish changed OFL profiles
id: scan
shell: bash shell: bash
env: env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCAN_UNTIL: ${{ steps.start.outputs.timestamp }}
run: | run: |
set -euo pipefail set -euo pipefail
published_any=false
mapfile -t branches < <( mapfile -t branches < <(
gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \ gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \
@@ -62,85 +87,53 @@ jobs:
for branch in "${branches[@]}"; do for branch in "${branches[@]}"; do
echo "::group::$branch" echo "::group::$branch"
# Use this workflow's own last successful run as the checkpoint. A # post_merge_profiles.yml's own run history, not this workflow's: this
# successful post_merge_profiles.yml run may record an OFL merge as # workflow only ever runs against main (schedule, or workflow_dispatch
# pending without publishing OFL, so its history must not advance # --ref main), so its head branch never varies - filtering ITS history
# this scan's checkpoint. Keep the branch filter aligned with the # by $branch would never match anything except main. post_merge_profiles.yml
# branch being inspected so each branch has its own checkpoint. # genuinely runs per-branch (this dispatch below sets --ref "$branch"),
# A failed daily run naturally gets retried from the previous # so its history is the real per-branch checkpoint. It also means a
# successful daily checkpoint; a branch with no prior run is # failed publish naturally gets retried tomorrow: the checkpoint only
# treated as changed below. # advances on a run that actually succeeded.
# --method GET is required, not cosmetic: gh api defaults to POST # --method GET is required, not cosmetic: gh api defaults to POST
# whenever -f fields are present unless a method is given # whenever -f fields are present unless a method is given
# explicitly, and POST on this list-runs endpoint 404s - confirmed # explicitly, and POST on this list-runs endpoint 404s - confirmed
# on real Actions infrastructure, not just reasoned about. # on real Actions infrastructure, not just reasoned about.
since="$(gh api --method GET "repos/${{ github.repository }}/actions/workflows/ofl-ota-cronjob.yml/runs" \ since="$(gh api --method GET "repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \
-f status=success -f branch="$branch" -f per_page=1 \ -f status=success -f branch="$branch" -f per_page=1 \
--jq '.workflow_runs[0].run_started_at // empty')" --jq '.workflow_runs[0].run_started_at // empty')"
if [ -z "$since" ]; then if [ -z "$since" ]; then
echo "No prior successful run for $branch; treating OFL as changed." echo "No prior successful run for $branch; checking OFL changes up to $SCAN_UNTIL."
changed=true changed_files="$(git log --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
else
changed_files="$(git log --since="$since" --name-only --pretty=format: "origin/$branch" -- \
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \ resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
| sed '/^$/d')" | sed '/^$/d')"
if [ -n "$changed_files" ]; then else
echo "OFL changed on $branch since $since:" changed_files="$(git log --since="$since" --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
echo "$changed_files" resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
changed=true | sed '/^$/d')"
else fi
echo "No OFL changes on $branch since $since."
changed=false if [ -n "$changed_files" ]; then
fi echo "OFL changed on $branch from ${since:-the beginning} through $SCAN_UNTIL:"
echo "$changed_files"
changed=true
else
echo "No OFL changes on $branch through $SCAN_UNTIL."
changed=false
fi fi
if [ "$changed" = true ]; then if [ "$changed" = true ]; then
# Tolerate a per-branch failure (e.g. a pre-existing release branch # Tolerate a per-branch failure (e.g. a pre-existing release branch
# whose post_merge_profiles.yml predates the vendor/auto_publish # whose post_merge_profiles.yml predates the vendor/auto_publish
# inputs) rather than aborting the whole scan under set -e. # inputs) rather than aborting the whole scan under set -e.
if gh workflow run post_merge_profiles.yml \ if ! gh workflow run post_merge_profiles.yml \
--repo "${{ github.repository }}" \ --repo "${{ github.repository }}" \
--ref "$branch" \ --ref "$branch" \
-f vendor="$VENDOR" -f auto_publish=true; then -f vendor="$VENDOR" -f auto_publish=true; then
published_any=true
else
echo "::warning::failed to dispatch post_merge_profiles.yml for $branch - its post_merge_profiles.yml at this ref may predate the vendor/auto_publish inputs" echo "::warning::failed to dispatch post_merge_profiles.yml for $branch - its post_merge_profiles.yml at this ref may predate the vendor/auto_publish inputs"
fi fi
fi fi
echo "::endgroup::" echo "::endgroup::"
done done
echo "published_any=$published_any" >> "$GITHUB_OUTPUT"
- name: Clear OFL pending queue
# Only when this run actually kicked off at least one publish - the
# daily reset is scoped to today's real activity, not called on a day
# where every branch reported no changes. Note "published_any" reflects
# a successful DISPATCH, not a confirmed live publish: gh workflow run
# is fire-and-forget, so this workflow never learns whether the
# dispatched post_merge_profiles.yml run actually reached its own
# auto-publish call. Acceptable since the table is populated only by
# our own auto-publish calls, not by anything else.
if: steps.scan.outputs.published_any == 'true'
shell: bash
env:
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
run: |
set -euo pipefail
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear" \
-H "Authorization: Bearer $OTA_API_KEY")"
body="$(cat "$resp_file")"
echo "$body"
if [ "$status" != "200" ]; then
echo "::error::OTA pending-clear call failed with HTTP $status"
exit 1
fi
+7 -2
View File
@@ -56,9 +56,9 @@ You can do this in Environment Variables settings.
endif () endif ()
if (APPLE) if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3 # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET) if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE) set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
endif () endif ()
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif () endif ()
@@ -279,6 +279,11 @@ if (APPLE)
endif() endif()
SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer") SET(CMAKE_XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.orcaslicer.OrcaSlicer")
# The macOS CI jobs build with Ninja (build_release_macos.sh -x), so the Xcode generator
# is not covered. Xcode adds -Wshorten-64-to-32 by default ("Implicit Conversion to 32 Bit
# Type"); Ninja/-Wall does not, and under -Werror it fails Xcode builds on code CI accepts.
set(CMAKE_XCODE_ATTRIBUTE_GCC_WARN_64_TO_32_BIT_CONVERSION "NO")
message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}") message(STATUS "Orca: IS_CROSS_COMPILE: ${IS_CROSS_COMPILE}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux") elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN") set(CMAKE_INSTALL_RPATH "$ORIGIN")
+2 -2
View File
@@ -53,7 +53,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -s: Build slicer only" echo " -s: Build slicer only"
echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)" echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)"
echo " -n: Nightly build" echo " -n: Nightly build"
echo " -t: Specify minimum version of the target platform, default is 11.3" echo " -t: Specify minimum version of the target platform, default is 12.0"
echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode" echo " -x: Use Ninja Multi-Config CMake generator, default is Xcode"
echo " -b: Build without reconfiguring CMake" echo " -b: Build without reconfiguring CMake"
echo " -c: Set CMake build configuration, default is Release" echo " -c: Set CMake build configuration, default is Release"
@@ -95,7 +95,7 @@ if [ -z "$DEPS_CMAKE_GENERATOR" ]; then
fi fi
if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then
export OSX_DEPLOYMENT_TARGET="11.3" export OSX_DEPLOYMENT_TARGET="12.0"
fi fi
if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then
+2 -2
View File
@@ -26,9 +26,9 @@ endif()
cmake_minimum_required(VERSION 3.2) cmake_minimum_required(VERSION 3.2)
if (APPLE) if (APPLE)
# if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 11.3 # if CMAKE_OSX_DEPLOYMENT_TARGET is not set, set it to 12.0 (the lowest Xcode 27 accepts)
if (NOT CMAKE_OSX_DEPLOYMENT_TARGET) if (NOT CMAKE_OSX_DEPLOYMENT_TARGET)
set(CMAKE_OSX_DEPLOYMENT_TARGET "11.3" CACHE STRING "Minimum OS X deployment version" FORCE) set(CMAKE_OSX_DEPLOYMENT_TARGET "12.0" CACHE STRING "Minimum OS X deployment version" FORCE)
endif () endif ()
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}") message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
+102 -185
View File
@@ -2,219 +2,136 @@
## Why it exists ## Why it exists
The main window is a notebook of tabs. When the first frame appears, one tab is on The main window is a notebook of tabs, and only one of them is on screen when the first
screen and the others are not; some of them are opened later in the session, some frame appears. Others are opened later in the session, some never, and some only exist
never, and some only exist for certain printers. Building a tab before the first frame for certain printers. Building every tab before the first frame makes each startup pay for
adds its cost to every startup, whether or not the tab is used. tabs the user may never open.
This subsystem builds a tab's panel the first time the tab is shown. It also builds the This subsystem builds a tab the first time it is shown, and builds the rest in small units
remaining tabs while the user is idle after startup, in units of tens of milliseconds, while the user is idle after startup. Startup pays only for what the first frame shows,
so a click that lands in the middle of one waits for that unit and no longer. Startup the other tabs are usually ready before anyone opens them, and a click that lands in the
pays only for what the first frame shows, and the other tabs are usually built before middle of the idle build waits for one unit at most. Work that is not a tab, such as a
anyone opens them. dialog or the 3D view's GL resources, uses the same machinery.
## The parts ## The parts
`src/slic3r/GUI/Lazy.hpp`, `LazyPage.hpp`, `StagedBuild.hpp` and `IdleScheduler.hpp/.cpp` The parts are independent. A holder can hold anything a factory makes, a placeholder page
(with its wx-free `PrebuildQueue.hpp`) are independent. A holder can hold anything a is a holder with a widget, a staged object can live outside a holder, and the scheduler
factory makes, a page is a holder with a placeholder widget, a staged object can live knows none of them; it runs tasks, which the main window makes from the holders.
outside a holder, and the scheduler knows none of them; it runs tasks, which `MainFrame`
makes from the holders.
### Lazy<T>: the holder ### The holder: `Lazy<T>`
Holds an object that a factory makes on first use, under a name for the log and a place A holder keeps one object and the factory that makes it. The rest of the app reads the
in the idle queue, both given by the owner. The header has no wx dependency; the busy object if it exists, makes sure it exists now when about to show or navigate to it, or runs
cursor for an on-demand build lives in `Lazy.cpp` and is skipped when there is no app, something once it exists. A type with one instance in the app gets these as statics
so the holder is unit-tested. through `LazyInstance<T>`, so callers need no reference to the main window, and all of them
are harmless while no holder exists.
- `get()` is null until the object is completely built; `built()` says the same. The holder guarantees that callers see the object only once it is completely built. A
- `ensure()` builds whatever is left now, under a busy cursor, and returns the object, or build cannot re-enter itself, so a nested request finds nothing yet, and a factory that
null in the two cases below. A click on an unbuilt tab or a first open of a dialog goes returns null or a unit that throws leaves the holder and the scheduler able to carry on.
through this, and it logs the units and time it took. The holder does not own the object; its wx parent does, as for any window. The holder has
- `build_step()` runs one unit of construction and returns true while more remain. The no wx dependency and is unit-tested.
first unit is the factory call, and each later unit is one `StagedBuild` step if the
type has them.
- `when_built(fn)` runs `fn(object)` now if it exists, otherwise once it is built.
- `pending()` says whether the idle prebuild has work here: not built, and the factory
has not returned null. A null factory result is logged and the holder stays unbuilt.
- A unit that pumps the event loop cannot re-enter the holder; a nested `build_step()`
does nothing and a nested `ensure()` returns null.
- After a unit throws, the holder and the scheduler still run the next one.
The holder does not own the object; its wx parent does, as for any window. A type with ### The placeholder page: `LazyPage<Panel>`
one instance in the app derives from `LazyInstance<T>`, which points at that instance's
holder (the holder registers itself, and a recreated `MainFrame`'s holder replaces the
old frame's) and gives the type the static entry points the rest of the app uses,
`T::if_built()`, `T::ensure()` and `T::when_built(fn)`. They return null, or do
nothing, while no holder exists, so a caller needs no `mainframe` check.
`built()` is an atomic flag, since a job worker reads it through the statics The notebook needs a page object for a tab to exist and for tabs to be inserted and
(`MainFrame::get_calibration_curr_tab()`); it is set after the object is complete. removed by pointer, and the placeholder is that object. It builds the real panel inside
itself the first time it is shown and forwards showing and hiding afterwards, so a panel's
own show handling stays its activation hook. Nothing builds while the main window is
hidden; the window's first show builds the start page. A page that is out of the book is
not prebuilt. A panel built while its page is hidden stays hidden, and gets the theming
the window applied before the panel existed.
`LazyBase` is the holder's type-free interface (`name()`, `built()`, `pending()`, ### Staged construction: `StagedBuild`
`build_step()`, `prebuild_order()`) and is what the scheduler side sees.
### LazyPage<Panel>: the placeholder A constructor too big to be one unit builds a skeleton and queues the rest as steps, which
run one per unit. A child panel's steps can be forwarded to its parent, and the parent is
complete only once the child is. Nothing may use what a step builds before the last step
has run, so staged panels follow these constraints:
A `wxPanel` placed in the parent in place of the real panel, and a `Lazy<Panel>` whose - members created in steps start out null, so a partly built panel can be destroyed;
factory makes the panel inside it (by default `new Panel(parent)`). The notebook needs a - timers, event handlers and destructors that touch step content check that the panel is
page object for the tab to exist and for `show_device()` to insert and remove tabs by complete first;
pointer, and the placeholder is that object. `MainFrame` creates every page once, named - nothing takes focus while off screen, since a unit may run while the user is typing
after its `TAB_ID_*`, and keeps it for the frame's life; `show_device()` only moves
pages in and out of the book.
- `Show()` is forwarded to the panel, so a panel's own `Show()` override stays its
activation hook (refresh timers, machine sync) and `SelectPageByName()` works
unchanged. A show builds the panel unless the frame itself is still hidden, because
the book selects its first page as it is inserted; `MainFrame::Show()` shows the
current page again when the frame becomes visible, which builds it.
- `in_book()` says whether the parent notebook currently lists the page, and
`pending()` is that and not built, so a tab `show_device()` has taken out of the book
is not prebuilt.
- A panel built while its page is hidden stays hidden, and a completed panel gets the
dark-mode pass the frame ran before it existed.
The Compare presets dialog is a holder without a page. `MainFrame` keeps a
`Lazy<DiffPresetDialog>` whose factory constructs the dialog and binds its events, the
dialog derives from `LazyInstance`, and its callers use
`DiffPresetDialog::ensure()->show()` and `DiffPresetDialog::if_built()` like a tab's
callers do. Saving a preset refreshes the dialog only while it is shown, since `show()`
reloads the presets.
### StagedBuild: construction in units
A mixin for a panel whose constructor is too big to be one unit. The constructor builds
the skeleton (sizers, and the parts other code may touch) and queues the rest with
`add_build_step()`. `build_step()` runs one step, and `add_build_steps_of(child)`
forwards a child's steps so a nested panel is spread the same way; the parent is built
only once the child is, including steps the child queues later. Steps run in order, on
the main thread. A `Lazy<T>` recognises a staged type at compile time and runs its steps
one per unit.
Nothing may touch what a step builds before the last step has run. In practice:
- nothing paints the panel before it is complete, since a panel built at idle is hidden
with its page and one built on demand finishes inside `ensure()` before the event loop
runs again;
- members created in steps are initialised to null in the header, so a partially built
panel can be destroyed;
- timers and event handlers that use step content check `built()` first
(`MonitorPanel::update_all()`, `CalibrationPanel::update_all()`), and a child's steps
are queued before anything in the constructor can fire such a handler;
- a destructor that disconnects from step content checks `built()` first;
- a constructor or step does not take focus while the panel is off screen
(`IsShownOnScreen()` before `SetFocus()`), since it may run while the user is typing
elsewhere; elsewhere;
- where a step's widgets must keep their place in a sizer that later steps also fill, the - a widget added by a step keeps its place in the sizer through an empty slot the skeleton
constructor adds an empty slot sizer in that position and the step fills the slot creates.
(`StatusBasePanel`).
### IdleScheduler: when to build ### The scheduler: `IdleScheduler` and `PrebuildQueue`
A task is any `LazyBase`: a name for the log, `pending()`, `build_step()` and The queue holds tasks in order, and a slice runs units of the first pending task until
`prebuild_order()`. `PrebuildQueue` holds the tasks by order (equal order in the order the task finishes, the time budget is spent, or input arrives. It has no wx dependency and
added) and runs a slice, the units of the first pending task until it completes, the is unit-tested with a fake clock. A task whose work goes away, such as a tab removed from
budget is spent on the clock it is given, or the interrupt predicate says input arrived. the book, is skipped, and becomes pending again if the work comes back.
It has no wx dependency and is unit-tested with a fake clock. A task whose work is gone
is passed over and stays in the queue, so a tab that `show_device()` removes and later
re-inserts is pending again.
`IdleScheduler` drives the queue with the real clock, the app's input timestamp and the A slice runs only once the user has been idle for a short quiet time, and each slice is its
Windows queue check, and logs each unit and slice. Each slice is a timer message: a own timer message, so paint, timers and input queued in between are handled before the
period of 250 ms while waiting for the user to go quiet, and a one-shot of zero after a next slice. Posting slices as pending events would not do that, because wx drains every
slice that left work, so the event loop dispatches whatever it has queued (paint, pending event before the next native message. On GTK a timer that is always due starves
timers, input) before the next slice runs. Chaining slices with `CallAfter` would not do the lower-priority sources that repaint and deliver posted events, so slices are a few
this: wx drains pending events fully before the next native message, on every platform. milliseconds apart. On Windows a slice also waits while the native queue holds input,
On GTK the one-shot is 5 ms, because a due GLib timeout runs ahead of the redraw and not counting mouse moves, which Windows synthesizes whenever a window appears under the
idle sources that paint and deliver posted events. cursor. A slice never runs inside a `wxYield()`, where it would build pages in the middle of
A slice runs only once the user has been idle for the quiet time, and the timer stops the code that yielded. A unit cannot be interrupted, so the largest unit bounds how long a
itself once no task is pending. The tick period, quiet time and slice length are click can wait.
constants in `IdleScheduler.cpp`. A unit cannot be interrupted once started, so the When nothing is pending the timer stops and the subsystem costs nothing.
largest unit bounds click latency on Windows; on the other platforms a click also waits
for the rest of the slice. A unit that pumps the event loop lets the timer fire inside
its own slice, and that tick does nothing.
The tasks are made by their owners. `MainFrame::prebuild_pages_when_idle()` registers The main window owns the scheduler because it owns what the tasks build, and clearing the
every `LazyPage` the frame created, in or out of the book (`pending()` is false for a queue with the window keeps a task from outliving its object. Each owner provides its own
page out of the book), the Compare presets holder, and the Prepare sidebar's settings tasks, such as a tab, a dialog, the Prepare tab's settings page one option group at a
page from `ParamsPanel::settings_page_prebuild()`, whose first unit selects the default time, the Prepare page's layout at the size the book gives its pages, or the 3D view's GL
tab if none is selected yet and whose later units build one option group each. resources.
`show_device()` only restarts the timer. `MainFrame` owns the scheduler because it owns
everything the tasks build, and clearing the queue with the frame is what keeps a task
from outliving its object.
Idle time comes from `GUI_App::FilterEvent`, which timestamps mouse and keyboard events ### The 3D view's GL resources
(`wxEVT_CATEGORY_USER_INPUT` minus command events, which all claim that category), and
`GUI_App::input_idle_ms()` reports it. On Windows a slice additionally refuses to start
when the message queue holds keyboard, button, touch or pen input. Mouse moves are
excluded because Windows synthesises one whenever a window appears under the cursor,
which every unit does. Other platforms use the timestamp alone.
Once every registered page is built the timer is stopped and the subsystem costs OpenGL is loaded on the Prepare tab's canvas. When the start page is not Prepare, loading
nothing. it is an idle task that makes the context current on the hidden canvas, so the start page
paints first and Prepare never appears. Loading it on a shown canvas under `Freeze()` holds
back the start page's paint, and on GTK `Freeze()` cannot hide the canvas, which is a
native child window or a Wayland subsurface drawn outside GTK. A hidden Windows child
window keeps its device context, macOS attaches the context to a hidden view, and GTK
creates the canvas's surface when the widget is realized, so on GTK the task realizes the
canvas first. If the context cannot be made current, the canvas's first render loads the
resources.
## Rules ## Rules
**What builds before the first frame.** The Prepare tab's plater, because `post_init()` **Before the first frame.** Only the start page and the Prepare tab's plater are built
needs its GL canvas on screen to initialise OpenGL in every startup state, and the start before the first frame. Everything else goes through a holder.
page the user configured. Home is built by the first `MainFrame::Show()`, Prepare's
settings page by selecting the tab. `post_init()` passes through the Prepare tab for GL
init under `Freeze()` with `MainFrame::select_prepare_for_gl_init()`, which changes the
selection without the page-changed event, so nothing else is built for that pass.
**Reaching a lazy object.** A caller uses the type's own statics. `T::if_built()` may **Reaching a lazy object.** Callers use the type's statics. Reading it if built is for
return null and is for telling the object something it can live without (a rescale, a things the object can live without, such as a rescale, a color change or a status refresh.
colour change, a status update). `T::ensure()` builds the object and is for navigating Making sure it exists is for navigating to it or showing it. Running something once it is
to it or for a caller that is about to show it. A caller that tells the object something built is for state it would not fetch for itself on construction. A panel that pulls its
it would not fetch for itself on construction uses `T::when_built()`, which keeps the state when constructed only ever needs to be read if built.
message until the object exists. A panel that pulls its state when constructed (the Home
page requests the recent list on load, the Device tab reads the device manager on show)
is reached with `if_built()`; one that cannot pull gets `when_built()`.
**Unit size.** A unit cannot be interrupted, so it should stay within the slice length on **Unit size.** A unit should fit in one slice on a fast machine. A constructor over that is
a fast machine. A constructor above that is staged. A single widget above it is the staged, and a single widget over it is accepted unless the widget itself can be split.
floor unless the widget itself is split.
**Order.** Cheapest and most likely to be opened first, given where `MainFrame` creates **Order.** Tasks run cheapest and most likely to be opened first. Each holder's order is
each holder. The settings page is 0 (the Prepare tab's own content), Home 10, Device 20 given where it is created, with gaps so a new task fits between its neighbors. A negative
(a Bambu user's usual second stop), Calibration 30, Multi-device 40, the web Device order is never prebuilt, for something few sessions open that costs more to build unasked
view 50, Project 60 (a second WebView2 instance) and the Compare presets dialog 100; than it saves.
steps of ten so a new tab takes a value between its neighbours without renumbering
them. `MainFrame::prebuild_pages_when_idle()` registers the tasks once, from
`post_init()`.
**Never prebuilt.** A holder with a negative order, for a tab that few sessions open
and that costs more to build unasked than it saves (the Design tab), and plugin-provided
tabs, which are Python-side and not lazy pages.
## Adopting it ## Adopting it
A lazy tab needs: A lazy tab needs a panel type deriving from `LazyInstance`, a placeholder page the main
window creates once with its order and registers for the idle build, and every use of the
panel outside the main window going through the statics. Its constructor has to cope with
the main window already existing and the user being busy elsewhere, so it takes no focus
while off screen, and it does all of its own setup, since the main window does nothing to a
panel after creating it.
1. The panel derived from `LazyInstance<Panel>`, since a tab's panel has one instance. To stage a heavy constructor, keep the skeleton in the constructor, move the rest into
2. A `LazyPage<Panel>*` member in `MainFrame`, created once in `init_tabpanel()` with steps in its original order, and follow the staged-construction constraints. Measure the
its `TAB_ID_*` as the name, its place by the order rule (and a factory if units; a step that is still one big widget is split inside the widget or accepted.
`new Panel(parent)` is not enough), added to `m_lazy_pages`, and used wherever the
tab is added to or looked up in the notebook.
3. Every use of the panel outside `MainFrame` going through one of the panel's statics,
chosen by the rule above. When converting an existing member, `grep` for it; the
compiler finds the rest.
4. A constructor that copes with the frame already existing and the user being busy
elsewhere, since `wxGetApp().mainframe` is set, the frame may be shown, and the user
may be typing when a lazy panel is built: no `SetFocus()` while off screen, and
whatever the constructor did through a `MainFrame` accessor before (a mode update, a
deferred URL) done in the constructor itself.
To stage a heavy constructor, inherit `StagedBuild`, keep the skeleton in the constructor, ## Verifying
move the rest into `add_build_step()` lambdas in the original order, and follow the
staged-panel rules above. Measure the units. A step that is still one big widget has to
be split inside that widget or accepted as the floor.
Verification is by log. `MainFrame::prebuild_pages_when_idle` lists the queue it The log lists the queue when it is registered, reports each completed task at info level
registered, `IdleScheduler::tick` reports each completed task by name at info level and and each slice and unit at debug level, and reports every on-demand build with its units
each slice and unit at debug level, and `Lazy::ensure` reports an object a user built and time. A task's completion line counts only the slice it finished in. A run from the
on demand with the units and time it took. A run from the configured start configured start page should show every registered task complete in order, with no unit
page shows every registered page complete, in order, with no unit longer than intended. longer than intended. A click on a tab during the idle build should show an on-demand
A click on a tab mid-prebuild shows the finished panel with an `ensure` line for what was build for what was left, with the slices resuming once the user is idle again.
left, and the slices resume for the remaining tasks once the user is idle again.
+123
View File
@@ -0,0 +1,123 @@
# Multiline infill — High Level Design
## Purpose and scope
`fill_multiline` prints every sparse infill wall as N adjacent lines instead of
one, so a wall is `d1 = N * spacing` thick. Only internal sparse infill uses it.
Each pattern first builds its single-line centerlines at N times the usual line
spacing (so the density holds), and `multiline_fill()` then replaces each
centerline by the lines of that wall: the centerline itself when N is odd, and
closed outlines around it at every `spacing` out to `d1 / 2`. The outlines are
clipped to the fill region contracted by half a line width, then connected like
any other infill.
Outlines of centerlines that cross each other overlap at every crossing, which
over-extrudes the wall intersections. The line-crossing patterns Grid,
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
Support Cubic (`FillAdaptive`); the other patterns outline their usual
centerlines.
## Non-crossing centerlines
The crossing lines are resolved into x-monotone paths, the levels of the line
arrangement: walking along x, the k-th path is always the k-th line from the
bottom. At every crossing, the two paths bounce off each other instead of
passing through. Adjacent paths meet only at crossings, so their outlines touch
there and nowhere overlap.
Where two paths meet, each is cut short by a line perpendicular to the bisector
of its bend, `d1 / 2` from the crossing. The two cut segments are parallel and
`d1` apart, so the outermost lines of the two walls sit exactly `spacing` apart,
like the lines inside a wall. Where three lines meet at one point, the middle
path runs straight through and the outer two are cut `d1` from it.
Each pattern builds its rows along x in a rotated frame. Grid lines run at ±45°
there, and its rows are trapezoid waves that transpose on alternate layers. The three families of Triangles, Tri-hexagon and
Cubic run at 0°, 60° and 120°. Those rows rotate by 120° every layer about a
3-fold center of the arrangement, so each family takes every role in turn.
The pattern is phased on fixed positions, so it lines up across layers and
across the regions of one layer. Rounding the corners with
`sparse_infill_smooth_factor` happens before `multiline_fill()`.
## Cubic
Single-line Cubic draws the three families at the same spacing `h` and shifts
them with z: by `+dx`, `-dx` and `+dx`, `dx = z / sqrt(2)`. The multiline paths
follow the same lines. In the frame where one family is horizontal, the other two
cross in rows `h` apart, alternating by half a period, at height
`tau = -3 * dx (mod h)` above the horizontal line below them. The crossings split
every band between horizontal lines into up-pointing triangles of height `tau`,
down-pointing triangles of height `h - tau`, and hexagons. At `tau = 0` (and `h`)
all three families meet at common points, as in Triangles. At `tau = h / 2` the
triangles are equal, as in Tri-hexagon. The origin of that frame is always a
3-fold center, whatever z is, so the per-layer rotation keeps the lines in place.
Each band holds two paths that touch at its crossings: the upper one takes the
V below the crossing and runs along the top horizontal line, and the lower one
takes the inverted V above it and runs along the bottom line. Both are the same function
of `tau`, the lower one mirrored with `h - tau`. `cubic_upper_level()` builds one
period of the upper path as the lower envelope of five lines, clipped from below:
- the two slanted lines through the crossings,
- the horizontal line, lowered when the triangle above it is less than `1.5 * d1` high,
- the two chamfers where the path turns onto and off the horizontal line, `d1 / 2`
from those crossings,
- the flat cut into the V at the crossing.
The cut height `clamp(tau - d1 / 2, 0, h - d1) + d1` is what makes the pattern
continuous in z. While both triangles are at least `1.5 * d1` high, every
crossing is a pair of bends `d1 / 2` from it, as in Tri-hexagon. When a triangle
is thinner, its three paths stack like a triple crossing. The path through it
flattens toward its base line and lies on it once the triangle is under `d1 / 2`
high, and the paths beside it are pushed `d1` away. The layout thus reaches the
Triangles one where the families meet. Adjacent paths stay at least `d1` apart
at every `tau` and at every density up to 100%.
## Adaptive Cubic
Adaptive Cubic and Support Cubic take their lines from an octree of cubes
standing on a corner. On each layer every cube cuts its three mid-planes into
segments of the same three 60° families as Cubic, but the pattern is not
periodic. Smaller cubes near the surface add finer lines, and a finer line ends
where it meets the wall of its coarser cube, so the lines form crossings and
T-junctions. `FillAdaptive::multiline_paths()` builds the paths from these
segments directly, for each fill region and within `4 * d1` of it.
At a crossing the two paths bounce as in Cubic. At a T-junction the through line
runs straight on and the path of the ending line stops there. Every path still
runs left to right in the frame where one family is horizontal, and that family
rotates with the layer.
Every line of every cube size lies on one fine lattice, so crossings closer than
a few `d1` are the corners of one small triangle of that lattice, as in Cubic.
The cuts follow the Cubic rules without a closed formula:
- The two bends of a crossing are cut `d1` apart, `d1 / 2` each, perpendicular
to their bisector, so their walls touch. A cut goes no further than the path
end, and the other bend takes the rest of `d1`.
- At the tip of a small triangle, between the two slanted families, a cut also
goes no further than the neighbouring bend turning the other way, and the
path beyond that bend is kept a wall away from it. The bends onto the
horizontal family are not limited this way: pushing their paths apart would
open gaps between walls that should touch.
- A cut moves the path only where the cut line lies beyond it, near its bend.
The sharp bends between the two slanted families are cut after the bends onto
the horizontal family, so the tip of a small triangle wins, as in Cubic.
- A path stopping at a T-junction is trimmed until it is `d1` less half a line
spacing from every other path, so that its end overlaps the wall it stops on
by half a line and bonds to it. The paths are trimmed one at a time against
the others as already trimmed, so two ends facing each other meet instead of
both backing off. A path stopping on the line of another is trimmed before
that one, so it gives way and the other still reaches the line it stops on. A
second round trims every path again from its full length, so an end grows
back where the ends it gave way to were trimmed later, and a last round only
shortens them, keeping them that far apart. Paths shorter than `d1` are left
out.
- A line that ends on another less than `2 * d1` past a crossing stops at that
crossing instead, the shorter one where both do. The path along such a stub
would be trimmed away, leaving a hole between the walls that were cut to
touch it.
Short paths enclosed by coarser lines still print as closed outlines, but most
paths run on across several cells.
+197
View File
@@ -0,0 +1,197 @@
# Prime tower sparse layers — High Level Design
## Purpose and scope
A prime tower exists to absorb filament changes, but it is planned on every
object layer below the topmost change, not only on the layers that purge. The
layers in between carry no filament change and print nothing but a block of the
tower's own footprint to keep its top level. They are called sparse layers, and
on a print with few changes they are most of the tower: they cost time, filament
and a travel to the tower on every layer.
Two settings trade that cost against something else. `wipe_tower_no_sparse_layers`
drops them, which sinks the tower below the model. `wipe_tower_sparse_layers_combination`
merges runs of them into fewer, thicker layers, which keeps the tower level with
the model. Both are off by default, and with both off the tower prints one layer
per object layer as it always has.
The decisions belong to tower planning and G-code emission. They do not change
sliced object geometry, but they do change the emitted G-code, the filament and
time estimates, and — for the compacted case — whether a plate is printable at
all. Changing either setting invalidates the tower step.
## What a sparse layer is
`ToolOrdering::fill_wipe_tower_partitions` counts the filament changes per layer
and propagates that count downwards, so every layer below the topmost change is
marked as carrying a tower. It then fills any gap between two tower layers, so
the tower is continuous from the bed to its last purge. `wipe_tower_layer_height`
is the distance from the previous tower layer, which is the object's layer height
whenever the tower prints on every layer.
`Print::_make_wipe_tower` plans one tower layer per such object layer. A layer
whose only call keeps the current filament leaves no toolchange in the plan, and
the layer it generates is a single result whose initial and new tool are equal.
That is what `wipe_tower_layer_is_sparse` recognises, and it is the unit both
settings work on.
The plan stays one entry per tower layer in every case. The G-code emitter walks
`WipeTowerData::tool_changes` by layer index, advancing once per object layer
that carries a tower, so a planner that removed entries would silently shift
every later layer onto the wrong tower geometry. Layers that print nothing are
therefore still planned and still generated; they are marked, and the emitter
drops them.
## Shared rules
Tower planning, G-code emission and the plate validation all have to agree about
which layers print and where. They ask one set of free functions, declared beside
the tower classes, rather than each re-deriving the answer from the raw options:
- `wipe_tower_sparse_layers_skipped` — whether sparse layers are really dropped.
Smooth timelapse and clumping detection park the nozzle on the tower every
layer, so with either of them on no layer is ever dropped and the option reads
as off everywhere.
- `wipe_tower_sparse_layers_combined` — whether runs are really merged. The same
two rule it out, and so does `wipe_tower_no_sparse_layers`: dropping the layers
outright is the stronger answer to the same problem, so the two settings are
exclusive and the GUI greys out the second while the first is on.
- `wipe_tower_layer_is_sparse`, `wipe_tower_layer_is_combined_away` — per-layer
questions the emitter asks about generated results.
- `compute_compacted_wipe_tower_z` — the tower's print z per planned layer when
it is compacted.
- `combine_sparse_wipe_tower_layers` and its `combine_sparse_wipe_tower_plan`
wrapper — the merge rule, applied to either generator's plan.
Both tower generators are driven through these. `WipeTower` (Type 1, the block
tower) and `WipeTower2` (Type 2, the default) keep separate plans with the same
per-layer shape — print z, layer height, toolchanges, and a `combined_away` flag
— so one template covers both.
## Dropping sparse layers
With `wipe_tower_no_sparse_layers`, the tower only grows on layers that carry a
real change. It therefore falls one layer height behind the object for every
sparse layer, and by the top of a tall print it can sit far below the model. The
nozzle has to reach down to it at each purge.
`compute_compacted_wipe_tower_z` derives that z once, from the generated results,
so the emitter and the validator cannot disagree. Emission descends to it, but
only once the nozzle is parked over the tower: descending while still over the
model would drive the nozzle into the print, so a descent that would do that is
deferred until after the travel to the tower. Extrusions emitted without an
explicit z — the nozzle-change wipe in particular — are pulled down to the
compacted z for the same reason.
Reaching down is only safe if nothing tall stands near the tower. `Print.hpp`
carries the clearance rule: a keep-out zone grown from the tower's footprint by
the spiral z-hop envelope, and a per-object limit on how high an object may rise
near it, tiered by the nozzle cone, the head body, the rod and the lid. The same
rule serves the precise check on real extrusions, the pre-slice estimate that
feeds the plater, and the outlines the plater draws while an object is dragged,
so that the ring the user sees touches the object's outline exactly when the
check trips.
## Merging sparse layers
With `wipe_tower_sparse_layers_combination`, no layer is dropped and nothing is
compacted: the tower keeps following the object, and the nozzle never descends.
Instead a run of consecutive sparse layers prints once, on the run's last layer,
at the accumulated height of everything it covers — the same way infill
combination merges sparse infill. The layers below it in the run print nothing.
`combine_sparse_wipe_tower_plan` runs before the tower's depths are planned,
because the heights it rewrites feed the extrusion flow of every later pass. It
raises `height` in place on the layer that prints a run and sets `combined_away`
on the rest; generation then proceeds unchanged, and the flag is copied onto the
results so the emitter can drop them.
Four constraints shape the rule:
- **Whole layers only.** A tower layer is entered at the object's z, so a merged
layer has to end on an object layer boundary. The merged height is therefore a
sum of whole layer heights, never a clamped value.
- **The nozzle's maximum layer height.** A run stops growing as soon as one more
layer would pass `max_layer_height` for the nozzle printing it — three quarters
of the nozzle diameter when that is left at 0, as elsewhere in slicing. The cap
is read through the filament-to-nozzle map, since `max_layer_height` is per
nozzle while the tower indexes filaments. This is what makes the setting inert
at common layer heights: two 0.2 mm layers are 0.4 mm and do not fit under a
0.3 mm maximum, so nothing merges until the layer height is 0.15 mm or below,
or the maximum is raised.
- **A filament change purges at its own z.** A layer with a real change can
neither be merged away nor absorb the run below it, so a run always ends on its
own last sparse layer and the change above it is untouched.
- **The first layer stays on the bed.** It carries the brim and is never merged.
A run holds one filament throughout — that is what makes it sparse — so the cap
is uniform across it, and the tower reserves depth only for the purges above a
layer, so a run has one footprint and the merged layer covers exactly the area
the layers it replaces would have.
## Emission and accounting
`WipeTowerIntegration` drops a layer whose results are marked, for both settings,
through the same `ignore_sparse` path in `tool_change` and
`is_empty_wipe_tower_gcode`. A dropped layer emits no travel to the tower and no
extrusion.
Filament used is accumulated by the generators while they write, so a layer that
will be dropped must not be charged. Type 1 asks `layer_is_printed` at each of
its accumulation points; Type 2 guards the equivalent block in `finish_layer`,
which also stops a merged-away layer from adding height of its own — the layer
that prints the run carries all of it.
A merged layer is the only case where the tower's layer height differs from the
object layer it sits on, and therefore the only case where the height the
exporter already emitted for that layer is wrong for the tower. Both generators
do declare a height, but each hardcodes a tag dialect — the block tower forces
the BBL tag, the other writes the compatible one — while the G-code processor
reads only the tag its printer uses. On a non-BBL printer with a Type 1 tower the
declaration is dropped, and the merged layer is drawn and costed as a thin one.
`WipeTowerIntegration::tower_height_tag` therefore declares it at export time,
where the printer is known, and only when the tower's own G-code does not already
carry the tag that will be read. The object's height returns on the next object
path, because emission forces the processor role to the tower on any layer that
carries one.
## Constraints
A layer that prints nothing prints nothing at all, including any interface work
the tower planner scheduled there. The Type 1 block planner marks a layer as a
contact layer when a filament category stops or starts being used relative to the
layer below, and a sparse layer immediately above a change qualifies. Merging a
run, like dropping its layers, replaces that interface with the run's single
layer. Both settings are off by default for this among other reasons.
Neither setting changes what the tower is for. A plate that needs a tower on
every layer — smooth timelapse, clumping detection — gets one, and the settings
read as off rather than compacting or merging in one place and not another.
## Implementation and verification
- [WipeTower.hpp](../../src/libslic3r/GCode/WipeTower.hpp) declares the shared
rules and the plan-merging template;
[WipeTower.cpp](../../src/libslic3r/GCode/WipeTower.cpp) implements them and
the Type 1 tower, [WipeTower2.cpp](../../src/libslic3r/GCode/WipeTower2.cpp)
the Type 2 tower.
- [ToolOrdering.cpp](../../src/libslic3r/GCode/ToolOrdering.cpp) decides which
layers carry a tower at all, and
[Print.cpp](../../src/libslic3r/Print.cpp) plans it and runs the clearance
check whose rule lives in [Print.hpp](../../src/libslic3r/Print.hpp).
- [GCode.cpp](../../src/libslic3r/GCode.cpp) emits the tower, drops the layers
that print nothing, and declares a merged layer's height;
[PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines the settings and
[ConfigManipulation.cpp](../../src/slic3r/GUI/ConfigManipulation.cpp) their
mutual exclusion.
- [GLCanvas3D.cpp](../../src/slic3r/GUI/GLCanvas3D.cpp) and
[PartPlate.cpp](../../src/slic3r/GUI/PartPlate.cpp) draw the compacted tower's
keep-out outlines live while the user drags.
- [Rule tests](../../tests/libslic3r/test_wipe_tower.cpp) cover the gating of
both settings, the per-layer predicates, the compacted z, the merge rule's run
flushing, height conservation, the nozzle cap and the first-layer exemption,
and the clearance geometry the plater draws.
- [Slicing tests](../../tests/fff_print/test_wipe_tower.cpp) slice a real print
and check that a run folds, that the tower still covers the object exactly
once, that a run too thin for the cap is left alone, and that a merged layer
declares its height in the tag the printer's processor reads.
+1 -1
View File
@@ -7,7 +7,6 @@
<script type="text/javascript" src="../include/jquery-2.1.1.min.js"></script> <script type="text/javascript" src="../include/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="../include/json2.js"></script> <script type="text/javascript" src="../include/json2.js"></script>
<script type="text/javascript" src="../include/globalapi.js"></script>
<link rel="stylesheet" type="text/css" href="../include/swiper/swiper-bundle.min.css" /> <link rel="stylesheet" type="text/css" href="../include/swiper/swiper-bundle.min.css" />
<script type="text/javascript" src="../include/swiper/swiper-bundle.min.js"></script> <script type="text/javascript" src="../include/swiper/swiper-bundle.min.js"></script>
@@ -18,6 +17,7 @@
<link rel="stylesheet" type="text/css" href="model.css" /> <link rel="stylesheet" type="text/css" href="model.css" />
<link rel="stylesheet" type="text/css" href="./css/dark.css" /> <link rel="stylesheet" type="text/css" href="./css/dark.css" />
<link rel="stylesheet" type="text/css" href="../include/global.css" /> <!-- ORCA One for all--> <link rel="stylesheet" type="text/css" href="../include/global.css" /> <!-- ORCA One for all-->
<script type="text/javascript" src="../include/globalapi.js"></script>
<script type="text/javascript" src="test.js"></script> <script type="text/javascript" src="test.js"></script>
<script type="text/javascript" src="model.js"></script> <script type="text/javascript" src="model.js"></script>
+6 -3
View File
@@ -697,7 +697,8 @@ public:
} }
} else { } else {
// Resize by duplicating the last value. // Resize by duplicating the last value.
this->values.resize(n, this->values./*back*/front()); T v = this->values./*back*/front();
this->values.resize(n, v);
} }
} }
} }
@@ -772,8 +773,10 @@ public:
if (this->values.empty()) if (this->values.empty())
this->values.resize(rhs_vec->size()); this->values.resize(rhs_vec->size());
else else {
this->values.resize(rhs_vec->size(), this->values.front()); T v = this->values.front();
this->values.resize(rhs_vec->size(), v);
}
assert(default_index.size() == rhs_vec->size()); assert(default_index.size() == rhs_vec->size());
+1
View File
@@ -990,6 +990,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() {
std::vector<std::wstring> font_names; std::vector<std::wstring> font_names;
EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack, EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack,
(LPARAM) &font_names); (LPARAM) &font_names);
ReleaseDC(NULL, hDC);
EmbossStyles font_list; EmbossStyles font_list;
for (const std::wstring &font_name : font_names) { for (const std::wstring &font_name : font_names) {
+29 -4
View File
@@ -464,6 +464,16 @@ void group_region_by_fuzzify(PerimeterGenerator& g)
} }
} }
g.fuzzy_supported_area.reset();
if ((g.has_fuzzy_skin || g.has_fuzzy_hole) && g.lower_slices != nullptr) {
coord_t max_thickness = 0;
for (const auto& region : regions)
if (should_fuzzify(region.config, g.layer_id, 0, true) || should_fuzzify(region.config, g.layer_id, 0, false))
max_thickness = std::max(max_thickness, region.config.thickness);
// Walls farther than a line width plus the noise amplitude from the layer below are bridging; keep them smooth.
g.fuzzy_supported_area = offset_ex(*g.lower_slices, float(g.ext_perimeter_flow.scaled_width() + max_thickness));
}
if (regions.size() == 1) { // optimization if (regions.size() == 1) { // optimization
g.regions_by_fuzzify.push_back({regions.front().config, {}}); g.regions_by_fuzzify.push_back({regions.front().config, {}});
return; return;
@@ -560,13 +570,23 @@ static std::vector<MergedFuzzyRegion> collect_merged_fuzzy_regions(const std::ve
return merged_regions; return merged_regions;
} }
// Afterwards an empty region means nothing to fuzzify, no longer full coverage.
static void restrict_to_supported(std::vector<MergedFuzzyRegion>& merged_regions, const std::optional<ExPolygons>& supported)
{
if (!supported)
return;
for (auto& merged_region : merged_regions)
merged_region.expolygons = merged_region.expolygons.empty() ? *supported : intersection_ex(merged_region.expolygons, *supported);
}
Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, const size_t loop_idx, const bool is_contour) Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perimeter_generator, const size_t loop_idx, const bool is_contour)
{ {
Polygon fuzzified; Polygon fuzzified;
const auto slice_z = perimeter_generator.slice_z; const auto slice_z = perimeter_generator.slice_z;
const auto& regions = perimeter_generator.regions_by_fuzzify; const auto& regions = perimeter_generator.regions_by_fuzzify;
if (regions.size() == 1) { // optimization const auto& supported = perimeter_generator.fuzzy_supported_area;
if (regions.size() == 1 && !supported) { // optimization
const auto& config = regions.begin()->first; const auto& config = regions.begin()->first;
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour); const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour);
if (!fuzzify) { if (!fuzzify) {
@@ -590,7 +610,7 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
// Fast path: single merged region — apply directly without splitting // Fast path: single merged region — apply directly without splitting
if (merged_regions.size() == 1) { if (merged_regions.size() == 1) {
const auto& mr = merged_regions.front(); const auto& mr = merged_regions.front();
if (mr.expolygons.empty()) { if (mr.expolygons.empty() && !supported) {
fuzzified = polygon; fuzzified = polygon;
fuzzy_polyline(fuzzified.points, true, slice_z, *mr.config); fuzzy_polyline(fuzzified.points, true, slice_z, *mr.config);
return fuzzified; return fuzzified;
@@ -626,6 +646,8 @@ Polygon apply_fuzzy_skin(const Polygon& polygon, const PerimeterGenerator& perim
if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty()) if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty())
merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons); merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons);
restrict_to_supported(merged_regions, supported);
// Split the loops into lines with different config, and fuzzy them separately // Split the loops into lines with different config, and fuzzy them separately
fuzzified = polygon; fuzzified = polygon;
for (const auto& r : merged_regions) { for (const auto& r : merged_regions) {
@@ -689,7 +711,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
const auto slice_z = perimeter_generator.slice_z; const auto slice_z = perimeter_generator.slice_z;
const auto layer_height = perimeter_generator.layer_height; const auto layer_height = perimeter_generator.layer_height;
const auto& regions = perimeter_generator.regions_by_fuzzify; const auto& regions = perimeter_generator.regions_by_fuzzify;
if (regions.size() == 1) { // optimization const auto& supported = perimeter_generator.fuzzy_supported_area;
if (regions.size() == 1 && !supported) { // optimization
const auto& config = regions.begin()->first; const auto& config = regions.begin()->first;
const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour); const bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
if (fuzzify) if (fuzzify)
@@ -703,7 +726,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
if (!merged_regions.empty()) { if (!merged_regions.empty()) {
// Fast path: single merged region — apply directly without splitting // Fast path: single merged region — apply directly without splitting
if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty()) { if (merged_regions.size() == 1 && merged_regions.front().expolygons.empty() && !supported) {
fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed); fuzzy_extrusion_line(extrusion->junctions, slice_z, perimeter_generator.layer_height, *merged_regions.front().config, closed);
return; return;
} }
@@ -753,6 +776,8 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty()) if (!merged_regions[i].expolygons.empty() && !merged_regions[j].expolygons.empty())
merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons); merged_regions[i].expolygons = diff_ex(merged_regions[i].expolygons, merged_regions[j].expolygons);
restrict_to_supported(merged_regions, supported);
// Split the loops into lines with different config, and fuzzy them separately // Split the loops into lines with different config, and fuzzy them separately
for (const auto& r : merged_regions) { for (const auto& r : merged_regions) {
const auto splitted = Algorithm::split_line(*extrusion, r.expolygons, false); const auto splitted = Algorithm::split_line(*extrusion, r.expolygons, false);
+572
View File
@@ -1,3 +1,4 @@
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp" #include "../ClipperUtils.hpp"
#include "../ExPolygon.hpp" #include "../ExPolygon.hpp"
#include "../Surface.hpp" #include "../Surface.hpp"
@@ -14,7 +15,9 @@
#include <cstdlib> #include <cstdlib>
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <functional>
#include <numeric> #include <numeric>
#include <tuple>
// Boost pool: Don't use mutexes to synchronize memory allocation. // Boost pool: Don't use mutexes to synchronize memory allocation.
#define BOOST_POOL_NO_MT #define BOOST_POOL_NO_MT
@@ -1318,6 +1321,564 @@ bool has_no_collinear_lines(const Polylines &polylines)
} }
#endif #endif
// Non-crossing centerlines for multiline adaptive cubic, see docs/HLSD/multiline-infill.md.
namespace noncrossing {
// y = x() * x + y() in the frame where the sweep family is horizontal.
using Lin = Vec2d;
static const Vec2d family_dir[3] { Vec2d(1., 0.), Vec2d(0.5, 0.5 * sqrt(3.)), Vec2d(0.5, -0.5 * sqrt(3.)) };
struct SweepLine
{
Vec2d a, b; // a.x() < b.x()
int family;
Lin lin;
std::vector<std::pair<double, int>> junctions; // (x, junction)
};
struct Junction
{
Vec2d p;
std::vector<int> lines;
std::vector<std::pair<int, int>> pairs; // (left, right) line of each path through, bottom-up
std::vector<std::pair<int, int>> bends; // (path, bend) of each pair, -1 where it runs straight
};
struct LevelPath
{
std::vector<Vec2d> verts; // start, bends, end
std::vector<int> lines; // line of each piece
std::vector<int> junctions; // junction of each bend
std::vector<int> turn; // 1 turning up, -1 turning down
std::vector<std::vector<Lin>> cuts;
std::vector<std::pair<Lin, int>> pushes; // (line, bend) keeping a wall away from a neighbour's cut
int start_term { -1 }; // junction where the path stops on another line, or -1
int end_term { -1 };
};
// Moves f onto line c (side 1: from below) wherever c lies beyond it, over the stretches overlapping [w0, w1].
static void clip_profile(std::vector<Vec2d> &f, const Lin &c, int side, double w0, double w1, double lim0, double lim1, bool cut_at_window, bool drop_past_limits)
{
const double r0 = std::max(lim0, f.front().x()), r1 = std::min(lim1, f.back().x());
if (r1 <= r0)
return;
const size_t ia = std::upper_bound(f.begin(), f.end(), r0, [](double x, const Vec2d &p) { return x < p.x(); }) - f.begin();
const size_t ib = std::lower_bound(f.begin() + ia, f.end(), r1, [](const Vec2d &p, double x) { return p.x() < x; }) - f.begin();
auto interpolate = [](const Vec2d &a, const Vec2d &b, double x) { return b.x() > a.x() ? a.y() + (x - a.x()) / (b.x() - a.x()) * (b.y() - a.y()) : b.y(); };
std::vector<Vec2d> local{ Vec2d(r0, interpolate(f[ia - 1], f[ia], r0)) };
local.insert(local.end(), f.begin() + ia, f.begin() + ib);
local.emplace_back(r1, interpolate(f[ib - 1], f[ib], r1));
const double tol = 1.;
auto beyond = [&c, side, tol](const Vec2d &p) { return side * (c.x() * p.x() + c.y() - p.y()) - tol; };
std::vector<std::pair<double, double>> stretches;
auto add = [&stretches](double x0, double x1) {
if (!stretches.empty() && stretches.back().second >= x0)
stretches.back().second = x1;
else
stretches.emplace_back(x0, x1);
};
for (size_t i = 1; i < local.size(); ++i) {
const Vec2d &p = local[i - 1], &q = local[i];
if (q.x() <= p.x())
continue;
const double bp = beyond(p), bq = beyond(q);
if (bp > 0. && bq > 0.)
add(p.x(), q.x());
else if (bp > 0. || bq > 0.) {
const double x = p.x() + bp / (bp - bq) * (q.x() - p.x());
if (bp > 0.)
add(p.x(), x);
else
add(x, q.x());
}
}
std::vector<std::pair<double, double>> keep;
for (auto [x0, x1] : stretches) {
if (x1 < w0 || x0 > w1)
continue;
if (drop_past_limits && ((x0 <= r0 && r0 == lim0) || (x1 >= r1 && r1 == lim1)))
continue;
keep.emplace_back(cut_at_window ? std::max(x0, w0) : x0, cut_at_window ? std::min(x1, w1) : x1);
}
if (keep.empty())
return;
auto y_local = [&](double x) {
size_t i = 1;
while (i + 1 < local.size() && local[i].x() < x)
++i;
return interpolate(local[i - 1], local[i], x);
};
std::vector<Vec2d> out(f.begin(), f.begin() + ia);
auto push = [&out, tol](double x, double y) {
if (out.empty() || x > out.back().x() || std::abs(y - out.back().y()) > 2. * tol)
out.emplace_back(x, y);
};
size_t k = 0;
for (const Vec2d &p : local) {
for (; k < keep.size() && keep[k].second < p.x(); ++k) {
const auto [x0, x1] = keep[k];
push(x0, y_local(x0));
push(x0, c.x() * x0 + c.y());
push(x1, c.x() * x1 + c.y());
push(x1, y_local(x1));
}
if (k < keep.size() && keep[k].first <= p.x() && p.x() <= keep[k].second)
continue;
push(p.x(), p.y());
}
for (; k < keep.size(); ++k) {
const auto [x0, x1] = keep[k];
push(x0, y_local(x0));
push(x0, c.x() * x0 + c.y());
push(x1, c.x() * x1 + c.y());
push(x1, y_local(x1));
}
for (size_t i = ib; i < f.size(); ++i)
push(f[i].x(), f[i].y());
f = std::move(out);
}
static std::vector<Vec2d> path_points(const LevelPath &path, const std::vector<SweepLine> &lines, double reach)
{
std::vector<Vec2d> f = path.verts;
const int nb = int(path.junctions.size());
auto x_of = [&path](int b) { return path.verts[b + 1].x(); };
auto sharp = [&](int b) { return lines[path.lines[b]].family != 0 && lines[path.lines[b + 1]].family != 0; };
// The run of bends turning the same way as bend b, up to the neighbouring bends turning the other way.
auto window = [&](int b) {
int l = b - 1, r = b + 1;
while (l >= 0 && path.turn[l] == path.turn[b])
--l;
while (r < nb && path.turn[r] == path.turn[b])
++r;
return std::make_pair(l >= 0 ? x_of(l) : f.front().x(), r < nb ? x_of(r) : f.back().x());
};
// Sharp bends between the slanted lines go last, so they win at the tip of a small triangle.
for (int b = 0; b < nb; ++b)
if (!sharp(b)) {
const auto [w0, w1] = window(b);
for (const Lin &c : path.cuts[b])
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, false);
}
for (int b = 0; b < nb; ++b)
if (sharp(b))
for (const Lin &c : path.cuts[b])
clip_profile(f, c, path.turn[b], x_of(b), x_of(b), x_of(b) - reach, x_of(b) + reach, false, true);
for (const auto &[c, b] : path.pushes) {
const auto [w0, w1] = window(b);
clip_profile(f, c, path.turn[b], w0, w1, x_of(b) - reach, x_of(b) + reach, true, true);
}
std::vector<Vec2d> pts;
for (const Vec2d &p : f) {
while (pts.size() >= 2 && std::abs(cross2(Vec2d(pts.back() - pts[pts.size() - 2]), Vec2d(p - pts.back()))) <=
1e-9 * (pts.back() - pts[pts.size() - 2]).norm() * (p - pts.back()).norm())
pts.pop_back();
pts.push_back(p);
}
return pts;
}
static double polyline_length(const std::vector<Vec2d> &pts)
{
double len = 0.;
for (size_t i = 1; i < pts.size(); ++i)
len += (pts[i] - pts[i - 1]).norm();
return len;
}
// Point at the given distance along pts, and the index of the segment it lies on.
static std::pair<Vec2d, size_t> point_along(const std::vector<Vec2d> &pts, double t)
{
for (size_t i = 1; i < pts.size(); ++i) {
const double len = (pts[i] - pts[i - 1]).norm();
if (t <= len)
return { pts[i - 1] + (len > 0. ? t / len : 0.) * (pts[i] - pts[i - 1]), i };
t -= len;
}
return { pts.back(), pts.size() - 1 };
}
} // namespace noncrossing
Polylines multiline_paths(const Lines &lines_in, double d1, double end_overlap, int sweep, const BoundingBox &cover)
{
using namespace noncrossing;
const double eps = scale_(0.002);
const Eigen::Rotation2Dd to_sweep(-sweep * M_PI / 3.);
const BoundingBoxf box(cover.min.cast<double>(), cover.max.cast<double>());
// Lines in the sweep frame, collinear pieces merged.
struct Piece { double c, s0, s1; };
std::array<std::vector<Piece>, 3> pieces;
for (const Line &line : lines_in) {
Vec2d a = line.a.cast<double>(), b = line.b.cast<double>();
if (!Geometry::liang_barsky_line_clipping(a, b, box) || (b - a).norm() < 10. * eps)
continue;
a = to_sweep * a;
b = to_sweep * b;
const double angle = std::atan2(b.y() - a.y(), b.x() - a.x()) / (M_PI / 3.);
if (std::abs(angle - std::round(angle)) > 0.01)
// Not one of the three families.
return {};
const int f = (int(std::round(angle)) % 3 + 3) % 3;
const Vec2d &d = family_dir[f];
const Vec2d n(-d.y(), d.x());
pieces[f].push_back({ n.dot(a), std::min(d.dot(a), d.dot(b)), std::max(d.dot(a), d.dot(b)) });
}
std::vector<SweepLine> lines;
for (int f = 0; f < 3; ++f) {
std::vector<Piece> &ps = pieces[f];
const Vec2d &d = family_dir[f];
const Vec2d n(-d.y(), d.x());
const double slope = d.y() / d.x();
std::sort(ps.begin(), ps.end(), [](const Piece &l, const Piece &r) { return l.c < r.c; });
for (size_t i = 0; i < ps.size();) {
size_t j = i + 1;
while (j < ps.size() && ps[j].c - ps[i].c < eps)
++j;
std::sort(ps.begin() + i, ps.begin() + j, [](const Piece &l, const Piece &r) { return l.s0 < r.s0; });
double c = 0.;
for (size_t k = i; k < j; ++k)
c += ps[k].c / double(j - i);
double s0 = ps[i].s0, s1 = ps[i].s1;
for (size_t k = i + 1; k <= j; ++k) {
if (k < j && ps[k].s0 <= s1 + eps) {
s1 = std::max(s1, ps[k].s1);
continue;
}
const Vec2d a = s0 * d + c * n;
lines.push_back({ a, s1 * d + c * n, f, Lin(slope, a.y() - slope * a.x()), {} });
if (k < j) {
s0 = ps[k].s0;
s1 = ps[k].s1;
}
}
i = j;
}
}
auto along = [](const SweepLine &l, const Vec2d &p) { return family_dir[l.family].dot(p - l.a); };
auto length = [](const SweepLine &l) { return (l.b - l.a).norm(); };
// Crossings, including the ends of lines stopping on another line.
std::vector<Junction> junctions;
auto detect_junctions = [&]() {
struct Hit { Vec2d p; int i, j; };
std::vector<Hit> hits;
std::vector<int> order(lines.size());
std::iota(order.begin(), order.end(), 0);
std::sort(order.begin(), order.end(), [&lines](int l, int r) { return lines[l].a.x() < lines[r].a.x(); });
for (size_t oi = 0; oi < order.size(); ++oi) {
const SweepLine &li = lines[order[oi]];
for (size_t oj = oi + 1; oj < order.size() && lines[order[oj]].a.x() <= li.b.x() + eps; ++oj) {
const SweepLine &lj = lines[order[oj]];
if (li.family == lj.family)
continue;
const double x = (lj.lin.y() - li.lin.y()) / (li.lin.x() - lj.lin.x());
const Vec2d p(x, li.lin.x() * x + li.lin.y());
const double ti = along(li, p), tj = along(lj, p);
if (ti > -eps && ti < length(li) + eps && tj > -eps && tj < length(lj) + eps)
hits.push_back({ p, order[oi], order[oj] });
}
}
std::sort(hits.begin(), hits.end(), [](const Hit &l, const Hit &r) { return l.p.x() < r.p.x(); });
junctions.clear();
for (const Hit &hit : hits) {
int found = -1;
for (int k = int(junctions.size()) - 1; k >= 0 && junctions[k].p.x() > hit.p.x() - eps; --k)
if (std::abs(junctions[k].p.y() - hit.p.y()) < eps) {
found = k;
break;
}
if (found < 0) {
found = int(junctions.size());
junctions.push_back({ hit.p, {}, {}, {} });
}
std::vector<int> &jl = junctions[found].lines;
for (int li : { hit.i, hit.j })
if (std::find(jl.begin(), jl.end(), li) == jl.end())
jl.push_back(li);
}
for (SweepLine &l : lines)
l.junctions.clear();
for (int ji = 0; ji < int(junctions.size()); ++ji)
for (int li : junctions[ji].lines)
lines[li].junctions.emplace_back(junctions[ji].p.x(), ji);
for (SweepLine &l : lines)
std::sort(l.junctions.begin(), l.junctions.end());
};
detect_junctions();
auto has_arm = [&](int ji, int li, bool right) {
const double t = along(lines[li], junctions[ji].p);
return right ? t < length(lines[li]) - eps : t > eps;
};
// A line ending on another just past a crossing stops at the crossing, where its stub would leave a hole.
auto crosses = [&](int ji, int li) { return has_arm(ji, li, false) && has_arm(ji, li, true); };
for (int pass = 0; pass < 3; ++pass) {
std::vector<bool> touched(lines.size(), false);
bool changed = false;
for (int ji = 0; ji < int(junctions.size()); ++ji) {
const Junction &J = junctions[ji];
if (J.lines.size() != 2 || touched[J.lines[0]] || touched[J.lines[1]] || !crosses(ji, J.lines[0]) || !crosses(ji, J.lines[1]))
continue;
// Shortest arm of each line from J to the junction where it ends on another line.
struct DeadArm { double length; bool at_b; int end; };
std::array<DeadArm, 2> dead;
dead.fill({ std::numeric_limits<double>::max(), false, -1 });
for (int k = 0; k < 2; ++k) {
const SweepLine &l = lines[J.lines[k]];
const size_t at = std::find_if(l.junctions.begin(), l.junctions.end(), [ji](const std::pair<double, int> &j) { return j.second == ji; }) - l.junctions.begin();
const double t = along(l, J.p);
if (at + 1 < l.junctions.size() && length(l) - along(l, junctions[l.junctions[at + 1].second].p) < eps)
dead[k] = { length(l) - t, true, l.junctions[at + 1].second };
if (at > 0 && along(l, junctions[l.junctions[at - 1].second].p) < eps && t < dead[k].length)
dead[k] = { t, false, l.junctions[at - 1].second };
}
const int k = dead[0].length <= dead[1].length ? 0 : 1;
if (dead[k].length >= 2. * d1)
continue;
SweepLine &l = lines[J.lines[k]];
(dead[k].at_b ? l.b : l.a) = J.p;
// Only the shortened line and those it ended on have stale junctions until the next pass.
for (int li : junctions[dead[k].end].lines)
touched[li] = true;
changed = true;
}
if (!changed)
break;
detect_junctions();
}
// At every crossing the lines bounce off each other, so that every path keeps running left to right.
for (int ji = 0; ji < int(junctions.size()); ++ji) {
Junction &J = junctions[ji];
std::vector<int> left, right;
for (int li : J.lines)
if (has_arm(ji, li, false) && has_arm(ji, li, true))
left.push_back(li);
right = left;
std::sort(left.begin(), left.end(), [&lines](int l, int r) { return lines[l].lin.x() > lines[r].lin.x(); });
std::sort(right.begin(), right.end(), [&lines](int l, int r) { return lines[l].lin.x() < lines[r].lin.x(); });
for (size_t k = 0; k < left.size(); ++k)
J.pairs.emplace_back(left[k], right[k]);
J.bends.assign(J.pairs.size(), { -1, -1 });
}
std::vector<LevelPath> paths;
for (int li = 0; li < int(lines.size()); ++li) {
const SweepLine &l = lines[li];
const int start = !l.junctions.empty() && !has_arm(l.junctions.front().second, li, false) ? l.junctions.front().second : -1;
LevelPath path;
path.start_term = start;
path.verts.push_back(start >= 0 ? junctions[start].p : l.a);
path.lines.push_back(li);
int cur = li;
double x = path.verts.front().x();
for (;;) {
const auto &js = lines[cur].junctions;
const auto it = std::find_if(js.begin(), js.end(), [x, eps](const std::pair<double, int> &j) { return j.first > x + 0.25 * eps; });
if (it == js.end()) {
path.verts.push_back(lines[cur].b);
break;
}
Junction &J = junctions[it->second];
const size_t k = std::find_if(J.pairs.begin(), J.pairs.end(), [cur](const std::pair<int, int> &p) { return p.first == cur; }) - J.pairs.begin();
if (k == J.pairs.size()) {
path.verts.push_back(J.p);
path.end_term = it->second;
break;
}
if (const int next = J.pairs[k].second; next != cur) {
J.bends[k] = { int(paths.size()), int(path.junctions.size()) };
path.verts.push_back(J.p);
path.lines.push_back(next);
path.junctions.push_back(it->second);
path.turn.push_back(lines[next].lin.x() > lines[cur].lin.x() ? 1 : -1);
cur = next;
}
x = it->first;
}
path.cuts.resize(path.junctions.size());
paths.push_back(std::move(path));
}
// Cut the two bends of a crossing d1 apart, no further than the neighbouring bends turning the other way.
for (const Junction &J : junctions) {
if (J.pairs.size() < 2 || J.bends.front().first < 0 || J.bends.back().first < 0)
continue;
const Vec2d n = (family_dir[lines[J.pairs.back().second].family] - family_dir[lines[J.pairs.back().first].family]).normalized();
auto cut = [&J, &n](double offset) {
const Vec2d q = J.p + offset * n;
const double s = -n.x() / n.y();
return Lin(s, q.y() - s * q.x());
};
LevelPath &lo = paths[J.bends.front().first], &hi = paths[J.bends.back().first];
const int lb = J.bends.front().second, hb = J.bends.back().second;
if (J.pairs.size() == 3) {
lo.cuts[lb].push_back(cut(-d1));
hi.cuts[hb].push_back(cut(d1));
continue;
}
// Only the tip of a small triangle, between the two slanted families, stops at its neighbouring bends.
const bool sharp = lines[J.pairs.front().first].family != 0 && lines[J.pairs.front().second].family != 0;
auto room = [&](const LevelPath &P, int b, int away, double sign) {
double c = std::numeric_limits<double>::max();
for (int nb : { b - 1, b + 1 })
if (sharp && nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away)
c = std::min(c, sign * n.dot(junctions[P.junctions[nb]].p - J.p));
if (b == 0 && P.start_term >= 0)
c = std::min(c, sign * n.dot(P.verts.front() - J.p));
if (b + 1 == int(P.junctions.size()) && P.end_term >= 0)
c = std::min(c, sign * n.dot(P.verts.back() - J.p));
return std::max(c, 0.);
};
const double c_lo = room(lo, lb, 1, -1.), c_hi = room(hi, hb, -1, 1.);
double d_lo = 0.5 * d1;
if (d1 - c_hi <= c_lo)
d_lo = std::clamp(d_lo, d1 - c_hi, c_lo);
else if (c_lo + c_hi > 0.)
d_lo = d1 * c_lo / (c_lo + c_hi);
const double d_hi = d1 - d_lo;
lo.cuts[lb].push_back(cut(-d_lo));
hi.cuts[hb].push_back(cut(d_hi));
auto propagate = [&](const LevelPath &P, int b, int away, int step, double offset) {
for (int nb : { b - 1, b + 1 })
if (nb >= 0 && nb < int(P.junctions.size()) && P.turn[nb] == away) {
const Junction &W = junctions[P.junctions[nb]];
const int k = int(std::find_if(W.pairs.begin(), W.pairs.end(), [&](const std::pair<int, int> &p) { return p.first == P.lines[nb]; }) - W.pairs.begin()) + step;
if (k >= 0 && k < int(W.bends.size()) && W.bends[k].first >= 0)
paths[W.bends[k].first].pushes.emplace_back(cut(offset), W.bends[k].second);
}
};
if (sharp) {
propagate(lo, lb, 1, -1, -d_lo - d1);
propagate(hi, hb, -1, 1, d_hi + d1);
}
}
std::vector<std::vector<Vec2d>> geometry;
Linesf segments;
std::vector<std::pair<int, double>> segment_start; // path, distance along it
std::vector<std::pair<double, double>> kept; // stretch of each path left by the trimming
for (const LevelPath &path : paths) {
geometry.push_back(path.junctions.empty() ? std::vector<Vec2d>{ path.verts.front(), path.verts.back() } : path_points(path, lines, 4. * d1));
double along_path = 0.;
for (size_t i = 1; i < geometry.back().size(); ++i) {
segments.emplace_back(geometry.back()[i - 1], geometry.back()[i]);
segment_start.emplace_back(int(geometry.size()) - 1, along_path);
along_path += (geometry.back()[i] - geometry.back()[i - 1]).norm();
}
kept.emplace_back(0., along_path);
}
// A path ending on another line stops end_overlap inside the walls of the others, as trimmed so far.
const double end_clearance = d1 - end_overlap;
AABBTreeLines::LinesDistancer<Linef> tree(segments);
auto clearance = [&](int pi, const Vec2d &q) {
double dist = std::numeric_limits<double>::max();
for (size_t s : tree.all_lines_in_radius(q, d1)) {
const auto [pj, start] = segment_start[s];
const Vec2d a = segments[s].a, d = segments[s].b - a;
const double len = d.norm(), t0 = std::max(0., kept[pj].first - start), t1 = std::min(len, kept[pj].second - start);
if (pj != pi && len > 0. && t0 <= t1)
dist = std::min(dist, line_alg::distance_to(Linef(a + t0 / len * d, a + t1 / len * d), q));
}
return dist;
};
// Returns the length trimmed off.
auto trim_front = [&](int pi, std::vector<Vec2d> &pts) {
const double total = polyline_length(pts), step = d1 / 32.;
double t = 0.;
while (t <= total && clearance(pi, point_along(pts, t).first) < end_clearance)
t += step;
if (t > total) {
pts.clear();
return total;
}
if (t == 0.)
return 0.;
for (double lo = std::max(0., t - step); t - lo > step / 256.;)
if (const double mid = 0.5 * (lo + t); clearance(pi, point_along(pts, mid).first) < end_clearance)
lo = mid;
else
t = mid;
const auto [q, seg] = point_along(pts, t);
pts.erase(pts.begin(), pts.begin() + (seg - 1));
pts.front() = q;
return t;
};
// A path stopping on the line of another path is trimmed first, so that it gives way to that path.
std::vector<std::vector<std::tuple<double, double, int>>> carried(lines.size()); // x range and path of each piece
for (int pi = 0; pi < int(paths.size()); ++pi)
for (size_t i = 0; i < paths[pi].lines.size(); ++i)
carried[paths[pi].lines[i]].emplace_back(paths[pi].verts[i].x(), paths[pi].verts[i + 1].x(), pi);
std::vector<std::vector<int>> stopping_on(paths.size());
for (int pi = 0; pi < int(paths.size()); ++pi)
for (const auto &[ji, own] : { std::make_pair(paths[pi].start_term, paths[pi].lines.front()), std::make_pair(paths[pi].end_term, paths[pi].lines.back()) })
if (ji >= 0)
for (int li : junctions[ji].lines)
if (li != own)
for (const auto &[x0, x1, pj] : carried[li])
if (pj != pi && x0 - eps <= junctions[ji].p.x() && junctions[ji].p.x() <= x1 + eps)
stopping_on[pj].push_back(pi);
std::vector<int> order;
std::vector<bool> visited(paths.size(), false);
std::function<void(int)> visit = [&](int pi) {
if (visited[pi])
return;
visited[pi] = true;
for (int child : stopping_on[pi])
visit(child);
order.push_back(pi);
};
for (int pi = 0; pi < int(paths.size()); ++pi)
visit(pi);
std::vector<std::vector<Vec2d>> trimmed(paths.size());
auto trim = [&](int pi) {
std::vector<Vec2d> &pts = trimmed[pi];
if (paths[pi].start_term >= 0)
kept[pi].first += trim_front(pi, pts);
if (paths[pi].end_term >= 0 && !pts.empty()) {
std::reverse(pts.begin(), pts.end());
kept[pi].second -= trim_front(pi, pts);
std::reverse(pts.begin(), pts.end());
}
if (pts.size() < 2 || polyline_length(pts) < d1) {
pts.clear();
kept[pi] = { 0., -1. };
}
};
// Ends grow back where the ends they gave way to were trimmed later; the last pass only shortens them.
for (int pass = 0; pass < 3; ++pass)
for (int pi : order) {
if (pass < 2) {
trimmed[pi] = geometry[pi];
kept[pi] = { 0., polyline_length(geometry[pi]) };
} else if (trimmed[pi].empty())
continue;
trim(pi);
}
Polylines out;
const Eigen::Rotation2Dd to_world = to_sweep.inverse();
for (const std::vector<Vec2d> &pts : trimmed) {
if (pts.empty())
continue;
Polyline pl;
for (const Vec2d &p : pts) {
const Vec2d w = to_world * p;
pl.points.emplace_back(coord_t(std::round(w.x())), coord_t(std::round(w.y())));
}
out.emplace_back(std::move(pl));
}
return out;
}
void Filler::_fill_surface_single( void Filler::_fill_surface_single(
const FillParams &params, const FillParams &params,
unsigned int thickness_layers, unsigned int thickness_layers,
@@ -1371,6 +1932,17 @@ void Filler::_fill_surface_single(
all_polylines.reserve(lines.size()); all_polylines.reserve(lines.size());
std::transform(lines.begin(), lines.end(), std::back_inserter(all_polylines), [](const Line& l) { return Polyline{ l.a, l.b }; }); std::transform(lines.begin(), lines.end(), std::back_inserter(all_polylines), [](const Line& l) { return Polyline{ l.a, l.b }; });
if (params.multiline > 1) {
const double d1 = scale_(this->spacing) * params.multiline;
BoundingBox cover = get_extents(expolygon);
cover.offset(coord_t(4. * d1));
// Rotate the family the paths run along with the layer, like the other multiline patterns.
const int sweep = int((this->layer_id / std::max(thickness_layers, 1u)) % 3);
// Line ends overlap the walls they stop on by half a line, so that they bond.
if (Polylines paths = multiline_paths(lines, d1, 0.5 * scale_(this->spacing), sweep, cover); !paths.empty())
all_polylines = std::move(paths);
}
// Apply multiline offset if needed // Apply multiline offset if needed
multiline_fill(all_polylines, params, spacing); multiline_fill(all_polylines, params, spacing);
+3
View File
@@ -48,6 +48,9 @@ FillAdaptive::OctreePtr build_octree(
// If true, octree is densified below internal overhangs only. // If true, octree is densified below internal overhangs only.
bool support_overhangs_only); bool support_overhangs_only);
// Multiline infill: lines of the three families to non-crossing paths d1 apart, ends reaching end_overlap into walls.
Polylines multiline_paths(const Lines &lines, double d1, double end_overlap, int sweep, const BoundingBox &cover);
// //
// Some of the algorithms used by class FillAdaptive were inspired by // Some of the algorithms used by class FillAdaptive were inspired by
// Cura Engine's class SubDivCube // Cura Engine's class SubDivCube
+99 -1
View File
@@ -3047,12 +3047,56 @@ bool FillRectilinear::fill_surface_by_multilines(const Surface *surface, FillPar
return true; return true;
} }
// Upper level of a cubic band [0, h] over one period, from the crossing at (0, tau) to the one at (period, tau).
// See docs/HLSD/multiline-infill.md.
static std::vector<Vec2d> cubic_upper_level(double tau, double h, double period, double d1)
{
const double s3 = std::sqrt(3.);
const double y_cut = std::clamp(tau - 0.5 * d1, 0., h - d1) + d1;
const double y_flat = std::min(h, h + y_cut - 2. * d1);
const double x2 = (h - tau) / s3 + d1;
const double x3 = (h + tau) / s3 - d1;
// (slope, intercept) of the rising line, its chamfer, the horizontal line, the falling chamfer and line.
const std::array<Vec2d, 5> lines{ Vec2d(s3, tau), Vec2d(1. / s3, h - x2 / s3), Vec2d(0., y_flat),
Vec2d(-1. / s3, h + x3 / s3), Vec2d(-s3, tau + s3 * period) };
auto y_at = [&lines, y_cut](double x) {
double y = std::numeric_limits<double>::max();
for (const Vec2d &l : lines)
y = std::min(y, l.x() * x + l.y());
return std::max(y, y_cut);
};
std::vector<double> xs;
for (size_t i = 0; i < lines.size(); ++i) {
if (lines[i].x() != 0.)
xs.emplace_back((y_cut - lines[i].y()) / lines[i].x());
for (size_t j = i + 1; j < lines.size(); ++j)
xs.emplace_back((lines[j].y() - lines[i].y()) / (lines[i].x() - lines[j].x()));
}
xs.erase(std::remove_if(xs.begin(), xs.end(), [period](double x) { return x <= 1. || x >= period - 1.; }), xs.end());
xs.insert(xs.end(), { 0., period });
std::sort(xs.begin(), xs.end());
xs.erase(std::unique(xs.begin(), xs.end(), [](double a, double b) { return b - a < 1.; }), xs.end());
std::vector<Vec2d> pts;
for (double x : xs) {
const Vec2d p(x, y_at(x));
if (pts.size() >= 2) {
const Vec2d &a = pts[pts.size() - 2], &b = pts.back();
if (std::abs((b.y() - a.y()) / (b.x() - a.x()) - (p.y() - b.y()) / (p.x() - b.x())) < EPSILON)
pts.pop_back();
}
pts.emplace_back(p);
}
return pts;
}
bool FillRectilinear::fill_surface_trapezoidal( bool FillRectilinear::fill_surface_trapezoidal(
const Surface* surface, const Surface* surface,
FillParams params, FillParams params,
const std::initializer_list<SweepParams>& sweep_params, const std::initializer_list<SweepParams>& sweep_params,
Polylines& polylines_out, Polylines& polylines_out,
int Pattern_type) // 0=grid, 1=triangular, 2=stars int Pattern_type) // 0=grid, 1=triangular, 2=stars, 3=cubic
{ {
assert(params.multiline > 1); assert(params.multiline > 1);
@@ -3350,6 +3394,55 @@ bool FillRectilinear::fill_surface_trapezoidal(
break; break;
} }
case 3: // Cubic
{
// Same z shifted lines as the single-line cubic; the slanted ones cross tau above the horizontal ones.
auto pos_mod = [](double a, double m) { const double r = std::fmod(a, m); return r < 0. ? r + m : r; };
const double h = 0.5 * std::sqrt(3.0) * period;
const double shift = scale_(std::sqrt(0.5) * this->z);
const double tau = pos_mod(-3. * shift, h);
const double y0 = pos_mod(-2. * shift, 2. * h);
std::array<std::vector<Vec2d>, 2> levels{ cubic_upper_level(h - tau, h, period, d1), cubic_upper_level(tau, h, period, d1) };
for (Vec2d &p : levels.front())
p.y() = h - p.y();
const size_t layer_mod = infill_layer_id % 3;
const double angle = layer_mod * 2.0 * M_PI / 3.0;
// Only cover the surface, seen in the frame the pattern is built in.
ExPolygon local = expolygon;
local.translate(-rotate_vector.second.x(), -rotate_vector.second.y());
if (layer_mod)
local.rotate(-angle);
BoundingBox cover = get_extents(local);
cover.offset(period);
const int64_t n_min = int64_t(std::floor((cover.min.y() - y0) / h)) - 1;
const int64_t n_max = int64_t(std::ceil((cover.max.y() - y0) / h)) + 1;
for (int64_t n = n_min; n <= n_max; ++n) {
const double x_off = (n & 1) ? 0.5 * period : 0.;
const double base = y0 + double(n) * h - tau;
const int64_t j_min = int64_t(std::floor((cover.min.x() - x_off) / period)) - 1;
const int64_t j_max = int64_t(std::ceil((cover.max.x() - x_off) / period));
for (const std::vector<Vec2d> &level : levels) {
Polyline row;
row.points.reserve(size_t(j_max - j_min + 1) * level.size());
for (int64_t j = j_min; j <= j_max; ++j)
for (size_t i = (j == j_min) ? 0 : 1; i < level.size(); ++i)
row.points.emplace_back(coord_t(std::round(x_off + double(j * period) + level[i].x())),
coord_t(std::round(base + level[i].y())));
polylines.emplace_back(std::move(row));
}
}
if (layer_mod)
for (Polyline &pl : polylines)
pl.rotate(angle, Point(0, 0));
break;
}
default: default:
// Handle unknown pattern type // Handle unknown pattern type
break; break;
@@ -3532,6 +3625,11 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams &para
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams &params) Polylines FillCubic::fill_surface(const Surface *surface, const FillParams &params)
{ {
Polylines polylines_out; Polylines polylines_out;
if (params.multiline > 1) {
if (!this->fill_surface_trapezoidal(surface, params, {}, polylines_out, 3))
BOOST_LOG_TRIVIAL(error) << "FillCubic::fill_surface_trapezoidal() failed.";
return polylines_out;
}
coordf_t dx = sqrt(0.5) * z; coordf_t dx = sqrt(0.5) * z;
if (! this->fill_surface_by_multilines( if (! this->fill_surface_by_multilines(
surface, params, surface, params,
+179 -169
View File
@@ -970,6 +970,27 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
return gcode; return gcode;
} }
// A folded tower layer is thicker than the object layer it sits on, so the height process_layer
// emitted is not the tower's. Both writers declare one, but each hardcodes a tag dialect - Type 1
// forces s_IsBBLPrinter and writes "; LAYER_HEIGHT:", Type 2 writes ";HEIGHT:" - and the processor
// reads only its printer's, so a Type 1 tower on a non-BBL printer loses it and the merged layer
// is drawn and costed as a thin one. Declare it here, where the printer is known, unless the tower
// already wrote the right tag. _extrude puts the object's height back on the next object path,
// since process_layer forces the role to erWipeTower on any layer with a tower.
std::string WipeTowerIntegration::tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr,
const std::string &tcr_gcode) const
{
const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height);
if (! m_sparse_layers_combined || std::abs(gcodegen.m_last_height - tcr.layer_height) <= EPSILON ||
tcr_gcode.find(tag) != std::string::npos)
return {};
// Keep m_last_height what the G-code last declared, so a second visit does not repeat it.
gcodegen.m_last_height = tcr.layer_height;
char buf[64];
sprintf(buf, "%s%g\n", tag.c_str(), tcr.layer_height);
return buf;
}
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)
@@ -1467,6 +1488,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
config.set_key_value("filament_start_gcode", new ConfigOptionString(start_filament_gcode_str)); config.set_key_value("filament_start_gcode", new ConfigOptionString(start_filament_gcode_str));
std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_filament_id, &config); std::string tcr_gcode, tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_filament_id, &config);
unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode);
gcode += tower_height_tag(gcodegen, tcr, tcr_gcode);
gcode += tcr_gcode; gcode += tcr_gcode;
// Count the toolchange only when the emitted block really changed the tool — // Count the toolchange only when the emitted block really changed the tool —
// tower visits without a filament change must not advance the ordinal. // tower visits without a filament change must not advance the ordinal.
@@ -1799,6 +1821,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
std::string tcr_gcode, std::string tcr_gcode,
tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config); tcr_escaped_gcode = gcodegen.placeholder_parser_process("tcr_rotated_gcode", tcr_rotated_gcode, new_extruder_id, &config);
unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode); unescape_string_cstyle(tcr_escaped_gcode, tcr_gcode);
gcode += tower_height_tag(gcodegen, tcr, tcr_gcode);
gcode += tcr_gcode; gcode += tcr_gcode;
check_add_eol(toolchange_gcode_str); check_add_eol(toolchange_gcode_str);
@@ -1946,7 +1969,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// Calculate where the wipe tower layer will be printed. -1 means that print z will not change, // Calculate where the wipe tower layer will be printed. -1 means that print z will not change,
// resulting in a wipe tower with sparse layers. // resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1; double wipe_tower_z = -1;
bool ignore_sparse = false; // Folded into a later, thicker layer that prints at its own z: nothing to emit.
bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]);
if (m_sparse_layers_skipped) { if (m_sparse_layers_skipped) {
wipe_tower_z = m_last_wipe_tower_print_z; wipe_tower_z = m_last_wipe_tower_print_z;
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0; ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]) && m_layer_idx != 0;
@@ -1964,7 +1988,8 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
// Calculate where the wipe tower layer will be printed. -1 means that print z will not change, // Calculate where the wipe tower layer will be printed. -1 means that print z will not change,
// resulting in a wipe tower with sparse layers. // resulting in a wipe tower with sparse layers.
double wipe_tower_z = -1; double wipe_tower_z = -1;
bool ignore_sparse = false; // Folded into a later, thicker layer that prints at its own z: nothing to emit.
bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]);
if (m_sparse_layers_skipped) { if (m_sparse_layers_skipped) {
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]); ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
wipe_tower_z = m_compacted_tower_z[m_layer_idx]; wipe_tower_z = m_compacted_tower_z[m_layer_idx];
@@ -1994,7 +2019,7 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
if (m_layer_idx >= (int) m_tool_changes.size()) if (m_layer_idx >= (int) m_tool_changes.size())
return true; return true;
bool ignore_sparse = false; bool ignore_sparse = wipe_tower_layer_is_combined_away(m_tool_changes[m_layer_idx]);
if (m_sparse_layers_skipped) if (m_sparse_layers_skipped)
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]); ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[m_layer_idx]);
@@ -3070,162 +3095,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
if (m_config.small_area_infill_flow_compensation.value && !m_config.small_area_infill_flow_compensation_model.empty()) if (m_config.small_area_infill_flow_compensation.value && !m_config.small_area_infill_flow_compensation_model.empty())
m_small_area_infill_flow_compensator = make_unique<SmallAreaInfillFlowCompensator>(print.config()); m_small_area_infill_flow_compensator = make_unique<SmallAreaInfillFlowCompensator>(print.config());
// Process file_start_gcode - written at the very top of the file, before any header
{
std::string top_gcode_template = print.config().file_start_gcode.value;
if (!top_gcode_template.empty()) {
DynamicConfig top_config;
// file_start_gcode runs before the parser copy that normally restores these, so set them here.
PlaceholderParser::update_timestamp(top_config);
PlaceholderParser::update_user_name(top_config);
top_config.set_key_value("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
top_config.set_key_value("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
top_config.set_key_value("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
top_config.set_key_value("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
top_config.set_key_value("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
top_config.set_key_value("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
std::string top_gcode = print.placeholder_parser().process(top_gcode_template, 0, &top_config);
if (!top_gcode.empty())
file.writeln(top_gcode);
}
}
// Orca: Don't output Header block if BTT thumbnail is identified in the list
// Get the thumbnails value as a string
std::string thumbnails_value = print.config().option<ConfigOptionString>("thumbnails")->value;
// search string for the BTT_TFT label
bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos);
if(!has_BTT_thumbnail){
file.write_format("; HEADER_BLOCK_START\n");
// Write information on the generator.
file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str());
if (is_bbl_printers)
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str());
//BBS: total layer number
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str());
//Orca: extra check for bbl printer
if (is_bbl_printers) {
if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode
// list all label_object_id with sorted order here
m_enable_exclude_object = true;
m_label_objects_ids.clear();
m_label_objects_ids.reserve(print.num_object_instances());
for (const PrintObject *print_object : print.objects())
for (const PrintInstance &print_instance : print_object->instances())
m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id());
std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end());
std::string objects_id_list = "; model label id: ";
for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++)
objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n"));
file.writeln(objects_id_list);
} else {
m_enable_exclude_object = false;
m_label_objects_ids.clear();
}
}
{
std::string filament_density_list = "; filament_density: ";
(filament_density_list+=m_config.filament_density.serialize()) +='\n';
file.writeln(filament_density_list);
std::string filament_diameter_list = "; filament_diameter: ";
(filament_diameter_list += m_config.filament_diameter.serialize()) += '\n';
file.writeln(filament_diameter_list);
coordf_t max_height_z = -1;
for (const auto& object : print.objects())
max_height_z = std::max(object->layers().back()->print_z, max_height_z);
std::ostringstream max_height_z_tip;
max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n';
file.writeln(max_height_z_tip.str());
}
{
auto used_filaments = print.get_slice_used_filaments(false);
std::ostringstream out;
out << "; filament: ";
for (size_t idx = 0; idx < used_filaments.size(); ++idx) {
if (idx != 0)
out << ',';
out << used_filaments[idx] + 1;
}
file.writeln(out.str());
}
file.write_format("; HEADER_BLOCK_END\n\n");
}
// BBS: write global config at the beginning of gcode file because printer
// need these config information
// Append full config, delimited by two 'phony' configuration keys
// CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured
// as configuration key / value pairs to be parsable by older versions of
// PrusaSlicer G-code viewer.
{
if (is_bbl_printers && !skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
// SoftFever: write compatiple image
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n",
first_layer_bed_temperature);
file.write_format(
"; first_layer_temperature = %d\n",
print.config().nozzle_temperature_initial_layer.get_at(0));
file.write("; CONFIG_BLOCK_END\n\n");
} else if (thumbnail_cb != nullptr) {
// generate the thumbnails
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = format("Invalid thumbnails value:");
error_str += GCodeThumbnails::get_error_string(errors);
throw Slic3r::ExportError(error_str);
}
if (!thumbnails.empty())
GCodeThumbnails::export_thumbnails_to_file(
thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); });
}
}
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
const double layer_height = first_object->config().layer_height.value;
const double initial_layer_print_height = print.config().initial_layer_print_height.value;
for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) {
const PrintRegion &region = print.get_print_region(region_id);
file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width());
file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width());
file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width());
file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width());
file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width());
if (print.has_support_material())
file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width());
if (print.config().initial_layer_line_width.value > 0)
file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width());
file.write_format("\n");
}
file.write_format("; EXECUTABLE_BLOCK_START\n");
// SoftFever
if( m_enable_exclude_object)
file.write(set_object_info(&print));
// adds tags for time estimators
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str());
// Prepare the helper object for replacing placeholders in custom G-code and output filename. // Prepare the helper object for replacing placeholders in custom G-code and output filename.
m_placeholder_parser_integration.parser = print.placeholder_parser(); m_placeholder_parser_integration.parser = print.placeholder_parser();
m_placeholder_parser_integration.parser.update_timestamp(); m_placeholder_parser_integration.parser.update_timestamp();
@@ -3360,16 +3229,6 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Orca: Initialise AdaptivePA processor filter // Orca: Initialise AdaptivePA processor filter
m_pa_processor = std::make_unique<AdaptivePAProcessor>(*this, tool_ordering.all_extruders()); m_pa_processor = std::make_unique<AdaptivePAProcessor>(*this, tool_ordering.all_extruders());
// Emit machine envelope limits for the Marlin firmware.
this->print_machine_envelope(file, print);
// Disable fan.
if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) {
file.write(m_writer.set_fan(0));
//BBS: disable additional fan
file.write(m_writer.set_additional_fan(0));
}
// Update output variables after the extruders were initialized. // Update output variables after the extruders were initialized.
m_placeholder_parser_integration.init(m_writer); m_placeholder_parser_integration.init(m_writer);
@@ -3721,6 +3580,157 @@ void GCode::_do_export(Print& print, GCodeOutputStream &file, ThumbnailsGenerato
// Sync variant-mapped params into placeholder_parser before processing start gcode // Sync variant-mapped params into placeholder_parser before processing start gcode
update_placeholder_parser_with_variant_params(); update_placeholder_parser_with_variant_params();
// Expand the file header only after the start-up placeholders and writer state are ready.
// Use the regular custom G-code path so invalid placeholders report the template name.
if (!print.config().file_start_gcode.value.empty())
file.writeln(this->placeholder_parser_process("file_start_gcode", print.config().file_start_gcode.value, initial_extruder_id));
// Orca: Don't output Header block if BTT thumbnail is identified in the list
// Get the thumbnails value as a string
std::string thumbnails_value = print.config().option<ConfigOptionString>("thumbnails")->value;
// search string for the BTT_TFT label
bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos);
if(!has_BTT_thumbnail){
file.write_format("; HEADER_BLOCK_START\n");
// Write information on the generator.
file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str());
if (is_bbl_printers)
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str());
//BBS: total layer number
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str());
//Orca: extra check for bbl printer
if (is_bbl_printers) {
if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode
// list all label_object_id with sorted order here
m_enable_exclude_object = true;
m_label_objects_ids.clear();
m_label_objects_ids.reserve(print.num_object_instances());
for (const PrintObject *print_object : print.objects())
for (const PrintInstance &print_instance : print_object->instances())
m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id());
std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end());
std::string objects_id_list = "; model label id: ";
for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++)
objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n"));
file.writeln(objects_id_list);
} else {
m_enable_exclude_object = false;
m_label_objects_ids.clear();
}
}
{
std::string filament_density_list = "; filament_density: ";
(filament_density_list+=m_config.filament_density.serialize()) +='\n';
file.writeln(filament_density_list);
std::string filament_diameter_list = "; filament_diameter: ";
(filament_diameter_list += m_config.filament_diameter.serialize()) += '\n';
file.writeln(filament_diameter_list);
coordf_t max_height_z = -1;
for (const auto& object : print.objects())
max_height_z = std::max(object->layers().back()->print_z, max_height_z);
std::ostringstream max_height_z_tip;
max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n';
file.writeln(max_height_z_tip.str());
}
{
auto used_filaments = print.get_slice_used_filaments(false);
std::ostringstream out;
out << "; filament: ";
for (size_t idx = 0; idx < used_filaments.size(); ++idx) {
if (idx != 0)
out << ',';
out << used_filaments[idx] + 1;
}
file.writeln(out.str());
}
file.write_format("; HEADER_BLOCK_END\n\n");
}
// BBS: write global config at the beginning of gcode file because printer
// need these config information
// Append full config, delimited by two 'phony' configuration keys
// CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured
// as configuration key / value pairs to be parsable by older versions of
// PrusaSlicer G-code viewer.
{
if (is_bbl_printers && !skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
// SoftFever: write compatiple image
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n",
first_layer_bed_temperature);
file.write_format(
"; first_layer_temperature = %d\n",
print.config().nozzle_temperature_initial_layer.get_at(0));
file.write("; CONFIG_BLOCK_END\n\n");
} else if (thumbnail_cb != nullptr) {
// generate the thumbnails
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = format("Invalid thumbnails value:");
error_str += GCodeThumbnails::get_error_string(errors);
throw Slic3r::ExportError(error_str);
}
if (!thumbnails.empty())
GCodeThumbnails::export_thumbnails_to_file(
thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); });
}
}
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
const double layer_height = first_object->config().layer_height.value;
const double initial_layer_print_height = print.config().initial_layer_print_height.value;
for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) {
const PrintRegion &region = print.get_print_region(region_id);
file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width());
file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width());
file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width());
file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width());
file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width());
if (print.has_support_material())
file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width());
if (print.config().initial_layer_line_width.value > 0)
file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width());
file.write_format("\n");
}
file.write_format("; EXECUTABLE_BLOCK_START\n");
// SoftFever
if( m_enable_exclude_object)
file.write(set_object_info(&print));
// adds tags for time estimators
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str());
// Emit machine envelope limits for the Marlin firmware.
this->print_machine_envelope(file, print);
// Disable fan.
if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) {
file.write(m_writer.set_fan(0));
//BBS: disable additional fan
file.write(m_writer.set_additional_fan(0));
}
std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id); std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id);
if (print.config().gcode_flavor != gcfKlipper) { if (print.config().gcode_flavor != gcfKlipper) {
// Set bed temperature if the start G-code does not contain any bed temp control G-codes. // Set bed temperature if the start G-code does not contain any bed temp control G-codes.
+6 -1
View File
@@ -107,7 +107,8 @@ public:
m_is_first_print(true), m_is_first_print(true),
m_print_config(&print_config), m_print_config(&print_config),
m_last_wipe_tower_print_z(print_config.z_offset.value), m_last_wipe_tower_print_z(print_config.z_offset.value),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config)) m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(print_config)),
m_sparse_layers_combined(wipe_tower_sparse_layers_combined(print_config))
{ {
// Precomputed rather than accumulated while emitting, so that the clearance validator and // Precomputed rather than accumulated while emitting, so that the clearance validator and
// the emitter cannot disagree about where the compacted tower sits on any given layer. // the emitter cannot disagree about where the compacted tower sits on any given layer.
@@ -138,6 +139,7 @@ public:
private: private:
WipeTowerIntegration& operator=(const WipeTowerIntegration&); WipeTowerIntegration& operator=(const WipeTowerIntegration&);
std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; std::string append_tcr(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
std::string tower_height_tag(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, const std::string &tcr_gcode) const;
Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const; Polyline generate_path_to_wipe_tower(const Point &start_pos, const Point &end_pos, const BoundingBox &avoid_polygon, const Polygons &bed_polygons) const;
std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const; std::string append_tcr2(GCode &gcodegen, const WipeTower::ToolChangeResult &tcr, int new_extruder_id, double z = -1.) const;
std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const; std::string travel_to_tower_gap(GCode &gcodegen, const Point &route_start, const Point &start_wipe_pos) const;
@@ -175,6 +177,9 @@ private:
// wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw // wipe_tower_no_sparse_layers, as answered by the shared compaction rule rather than by the raw
// option: smooth timelapse and wrapping detection keep a tower on every layer regardless. // option: smooth timelapse and wrapping detection keep a tower on every layer regardless.
const bool m_sparse_layers_skipped; const bool m_sparse_layers_skipped;
// Combined tower layers are thicker than the object layer they sit on, the only case where the
// tower's height is not the one process_layer already declared.
const bool m_sparse_layers_combined;
// Print z of the compacted tower per planned layer. Empty when the tower is not compacted. // Print z of the compacted tower per planned layer. Empty when the tower is not compacted.
std::vector<float> m_compacted_tower_z; std::vector<float> m_compacted_tower_z;
}; };
+73 -5
View File
@@ -49,6 +49,48 @@ std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<
return tower_z; return tower_z;
} }
bool wipe_tower_sparse_layers_combined(const PrintConfig &config)
{
return config.wipe_tower_sparse_layers_combination.value && ! wipe_tower_sparse_layers_skipped(config) &&
config.timelapse_type.value != TimelapseType::tlSmooth && ! config.enable_wrapping_detection.value;
}
bool wipe_tower_layer_is_combined_away(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes)
{
return ! layer_tool_changes.empty() && layer_tool_changes.front().combined_away;
}
std::vector<char> combine_sparse_wipe_tower_layers(std::vector<float> &layer_height,
const std::vector<char> &layer_is_sparse,
const std::vector<float> &max_layer_height,
size_t first_layer_idx)
{
assert(layer_is_sparse.size() == layer_height.size() && max_layer_height.size() == layer_height.size());
std::vector<char> combined_away(layer_height.size(), 0);
float pending_height = 0.f; // what the layers folded away so far add up to
for (size_t i = 0; i < layer_height.size(); ++i) {
// A toolchange has to purge at its own z, so it neither folds away nor takes over the run
// below it - and a run always flushes on its own last layer, so nothing is ever pending here.
if (! layer_is_sparse[i] || i <= first_layer_idx) {
pending_height = 0.f;
continue;
}
const float merged = pending_height + layer_height[i];
// Hand the run on only if the next layer can swallow the whole thing; a layer already past
// the cap is left alone rather than shrunk.
const bool next_takes_it = i + 1 < layer_height.size() && layer_is_sparse[i + 1] &&
merged + layer_height[i + 1] <= max_layer_height[i + 1] + float(EPSILON);
if (next_takes_it) {
combined_away[i] = 1;
pending_height = merged;
} else {
layer_height[i] = merged;
pending_height = 0.f;
}
}
return combined_away;
}
inline float align_round(float value, float base) inline float align_round(float value, float base)
{ {
return std::round(value / base) * base; return std::round(value / base) * base;
@@ -1904,6 +1946,7 @@ WipeTower::WipeTower(const PrintConfig& config, int plate_idx, Vec3d plate_origi
//m_bridging(float(config.wipe_tower_bridging)), //m_bridging(float(config.wipe_tower_bridging)),
m_bridging(10.f), m_bridging(10.f),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)),
m_gcode_flavor(config.gcode_flavor), m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_current_tool(initial_tool), m_current_tool(initial_tool),
@@ -2028,6 +2071,16 @@ void WipeTower::set_extruder(size_t idx, const PrintConfig& config)
float nozzle_diameter = float(config.nozzle_diameter.get_at(idx)); float nozzle_diameter = float(config.nozzle_diameter.get_at(idx));
m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM
// Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than
// by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp.
{
const std::vector<int> &filament_map = config.filament_map.values; // 1 based nozzle indices
const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0;
const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx));
m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height
: 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx));
}
float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx)); float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx));
if (max_vol_speed!= 0.f) if (max_vol_speed!= 0.f)
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); m_filpar[idx].max_e_speed = (max_vol_speed / filament_area());
@@ -3001,7 +3054,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer(bool extrude_perimeter, bool
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_sparse_layers_skipped || toolchanges_on_layer) if (layer_is_printed(toolchanges_on_layer))
if (m_current_tool < m_used_filament_length.size()) if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -3898,7 +3951,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer) if (layer_is_printed(toolchanges_on_layer))
if (m_current_tool < m_used_filament_length.size()) if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
@@ -4008,7 +4061,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer) if (layer_is_printed(toolchanges_on_layer))
if (filament_id < m_used_filament_length.size()) if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4125,7 +4178,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block_solid(const WipeTowerBlock &
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer) if (layer_is_printed(toolchanges_on_layer))
if (filament_id < m_used_filament_length.size()) if (filament_id < m_used_filament_length.size())
m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length(); m_used_filament_length[filament_id] += writer.get_and_reset_used_filament_length();
@@ -4668,6 +4721,15 @@ void WipeTower::calc_block_infill_gap()
m_extra_spacing = 1.f; m_extra_spacing = 1.f;
} }
// A folded layer is generated like any other but thrown away by the emitter, so its extrusions must
// not be charged to the filament used.
bool WipeTower::layer_is_printed(bool toolchanges_on_layer) const
{
if (m_layer_info != m_plan.end() && m_layer_info->combined_away)
return false;
return ! m_sparse_layers_skipped || toolchanges_on_layer;
}
void WipeTower::plan_tower_new() void WipeTower::plan_tower_new()
{ {
if (m_wipe_tower_brim_width < 0) m_wipe_tower_brim_width = get_auto_brim_by_height(m_wipe_tower_height); if (m_wipe_tower_brim_width < 0) m_wipe_tower_brim_width = get_auto_brim_by_height(m_wipe_tower_height);
@@ -4820,6 +4882,9 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
if (m_plan.empty()) if (m_plan.empty())
return; return;
//m_extra_spacing = 1.f; //m_extra_spacing = 1.f;
// Before planning: the layer heights this rewrites feed the extrusion flow of every later pass.
if (m_sparse_layers_combined)
combine_sparse_wipe_tower_plan(m_plan, m_filpar, m_first_layer_idx, m_current_tool);
m_wipe_tower_height = m_plan.back().z;//real wipe_tower_height m_wipe_tower_height = m_plan.back().z;//real wipe_tower_height
plan_tower_new(); plan_tower_new();
m_layer_info = m_plan.begin(); m_layer_info = m_plan.begin();
@@ -5014,6 +5079,9 @@ void WipeTower::generate_new(std::vector<std::vector<WipeTower::ToolChangeResult
if (only_generate_wall && !timelapse_wall.gcode.empty()) { if (only_generate_wall && !timelapse_wall.gcode.empty()) {
layer_result.insert(layer_result.begin(), std::move(timelapse_wall)); layer_result.insert(layer_result.begin(), std::move(timelapse_wall));
} }
if (layer.combined_away)
for (WipeTower::ToolChangeResult &tcr : layer_result)
tcr.combined_away = true;
result.emplace_back(std::move(layer_result)); result.emplace_back(std::move(layer_result));
} }
assert(m_outer_wall.size() == m_plan.size()); assert(m_outer_wall.size() == m_plan.size());
@@ -5179,7 +5247,7 @@ WipeTower::ToolChangeResult WipeTower::only_generate_out_wall(bool is_new_mode)
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (!m_sparse_layers_skipped || toolchanges_on_layer) if (layer_is_printed(toolchanges_on_layer))
if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); if (m_current_tool < m_used_filament_length.size()) m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
return construct_tcr(writer, false, old_tool, true, false, 0.f, false); return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
+67
View File
@@ -152,6 +152,10 @@ public:
bool is_contact = false; bool is_contact = false;
NozzleChangeResult nozzle_change_result; NozzleChangeResult nozzle_change_result;
// Orca: folded into a later, thicker layer, so the emitter drops it. Set by the tower, so
// the two cannot disagree about which layers print.
bool combined_away = false;
// Sum the total length of the extrusion. // Sum the total length of the extrusion.
float total_extrusion_length_in_plane() { float total_extrusion_length_in_plane() {
float e_length = 0.f; float e_length = 0.f;
@@ -391,6 +395,8 @@ public:
float filament_tower_interface_pre_extrusion_dist = 0; float filament_tower_interface_pre_extrusion_dist = 0;
float filament_tower_interface_pre_extrusion_length = 0; float filament_tower_interface_pre_extrusion_length = 0;
float filament_petg_pre_extrusion_offset_dist = 0; float filament_petg_pre_extrusion_offset_dist = 0;
// Tallest layer this filament's nozzle can lay down; caps the sparse layer combination.
float max_layer_height = 0.f;
}; };
@@ -522,6 +528,7 @@ private:
//float m_extra_loading_move = 0.f; //float m_extra_loading_move = 0.f;
float m_bridging = 0.f; float m_bridging = 0.f;
bool m_sparse_layers_skipped = false; bool m_sparse_layers_skipped = false;
bool m_sparse_layers_combined = false;
// BBS: remove useless config // BBS: remove useless config
//bool m_set_extruder_trimpot = false; //bool m_set_extruder_trimpot = false;
bool m_adhesion = true; bool m_adhesion = true;
@@ -595,6 +602,8 @@ private:
} }
// Calculates depth for all layers and propagates them downwards // Calculates depth for all layers and propagates them downwards
void plan_tower(); void plan_tower();
// Whether the layer reaches the G-code, and so whether its extrusions count as filament used.
bool layer_is_printed(bool toolchanges_on_layer) const;
// Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental // Goes through m_plan and recalculates depths and width of the WT to make it exactly square - experimental
void make_wipe_tower_square(); void make_wipe_tower_square();
@@ -634,6 +643,8 @@ private:
float depth; // depth of the layer based on all layers above float depth; // depth of the layer based on all layers above
float extra_spacing; float extra_spacing;
bool extruder_fill{true}; bool extruder_fill{true};
// Folded into a later, thicker layer, so this one prints nothing at all.
bool combined_away{false};
float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; } float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; }
std::vector<ToolChange> tool_changes; std::vector<ToolChange> tool_changes;
@@ -700,6 +711,62 @@ std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<
float base_z = 0.f); float base_z = 0.f);
// Combination rule for wipe_tower_sparse_layers_combination. Nothing is compacted - the tower keeps
// following the object - but a run of consecutive toolchange-free layers prints as one thicker layer,
// the way infill combination merges sparse infill. Shared so that neither tower generator nor the
// G-code emitter can combine on its own.
// Whether sparse layers are really combined. Skipping them outright is the stronger answer to the
// same problem and wins over this; smooth timelapse and wrapping detection need a tower on every
// layer, so they rule it out too.
bool wipe_tower_sparse_layers_combined(const PrintConfig &config);
// A planned layer folded into a later, thicker one prints nothing at all.
bool wipe_tower_layer_is_combined_away(const std::vector<WipeTower::ToolChangeResult> &layer_tool_changes);
// Folds runs of sparse layers into one. layer_height is raised in place on the layer that prints a
// run - always its last, so the merged extrusion lands on top of what it covers - and the returned
// mask marks the layers that now print nothing. A run stops growing once one more layer would pass
// max_layer_height of the nozzle that prints it. first_layer_idx and below never combine: the
// tower's first layer carries the brim.
std::vector<char> combine_sparse_wipe_tower_layers(std::vector<float> &layer_height,
const std::vector<char> &layer_is_sparse,
const std::vector<float> &max_layer_height,
size_t first_layer_idx);
// Applies the rule above to a planned tower. Either generator's plan fits: both carry height,
// tool_changes and combined_away per layer, and index their filament parameters by tool.
template<class PlanLayers, class FilamentParams>
void combine_sparse_wipe_tower_plan(PlanLayers &plan, const FilamentParams &filpar, size_t first_layer_idx, size_t initial_tool)
{
const size_t n = plan.size();
std::vector<float> heights(n);
std::vector<char> sparse(n);
std::vector<float> caps(n);
// A layer with no toolchange prints with the filament the layer below left loaded.
size_t tool = initial_tool;
for (const auto &layer : plan)
if (! layer.tool_changes.empty()) {
tool = layer.tool_changes.front().old_tool;
break;
}
for (size_t i = 0; i < n; ++i) {
heights[i] = plan[i].height;
sparse[i] = plan[i].tool_changes.empty() ? 1 : 0;
caps[i] = tool < filpar.size() ? filpar[tool].max_layer_height : 0.f;
if (! plan[i].tool_changes.empty())
tool = plan[i].tool_changes.back().new_tool;
}
const std::vector<char> combined_away = combine_sparse_wipe_tower_layers(heights, sparse, caps, first_layer_idx);
for (size_t i = 0; i < n; ++i) {
plan[i].height = heights[i];
plan[i].combined_away = combined_away[i] != 0;
}
}
} // namespace Slic3r } // namespace Slic3r
#endif // WipeTowerPrusaMM_hpp_ #endif // WipeTowerPrusaMM_hpp_
+22 -1
View File
@@ -1033,6 +1033,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_z_pos(0.f), m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)), m_bridging(float(config.wipe_tower_bridging)),
m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)), m_sparse_layers_skipped(wipe_tower_sparse_layers_skipped(config)),
m_sparse_layers_combined(wipe_tower_sparse_layers_combined(config)),
m_gcode_flavor(config.gcode_flavor), m_gcode_flavor(config.gcode_flavor),
m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))), m_infill_speed(default_region_config.sparse_infill_speed.get_at(get_extruder_index(config, (unsigned int)initial_tool))),
@@ -1150,6 +1151,16 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config)
float nozzle_diameter = float(config.nozzle_diameter.get_at(idx)); float nozzle_diameter = float(config.nozzle_diameter.get_at(idx));
m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM m_filpar[idx].nozzle_diameter = nozzle_diameter; // to be used in future with (non-single) multiextruder MM
// Orca: max_layer_height is per nozzle, so read it through the filament->nozzle map rather than
// by filament id. Zero means three quarters of the nozzle diameter, as in Slicing.cpp.
{
const std::vector<int> &filament_map = config.filament_map.values; // 1 based nozzle indices
const size_t nozzle_idx = idx < filament_map.size() && filament_map[idx] > 0 ? size_t(filament_map[idx] - 1) : 0;
const float max_layer_height = float(config.max_layer_height.get_at(nozzle_idx));
m_filpar[idx].max_layer_height = max_layer_height > 0.f ? max_layer_height
: 0.75f * float(config.nozzle_diameter.get_at(nozzle_idx));
}
float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx)); float max_vol_speed = float(config.filament_max_volumetric_speed.get_at(idx));
if (max_vol_speed!= 0.f) if (max_vol_speed!= 0.f)
m_filpar[idx].max_e_speed = (max_vol_speed / filament_area()); m_filpar[idx].max_e_speed = (max_vol_speed / filament_area());
@@ -2103,7 +2114,9 @@ WipeTower::ToolChangeResult WipeTower2::finish_layer()
// Ask our writer about how much material was consumed. // Ask our writer about how much material was consumed.
// Skip this in case the layer is sparse and config option to not print sparse layers is enabled. // Skip this in case the layer is sparse and config option to not print sparse layers is enabled.
if (! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) { // A folded layer prints nothing, so it consumes nothing and adds no height of its own.
const bool combined_away = m_layer_info != m_plan.end() && m_layer_info->combined_away;
if ((! m_sparse_layers_skipped || toolchanges_on_layer || first_layer) && ! combined_away) {
if (m_current_tool < m_used_filament_length.size()) if (m_current_tool < m_used_filament_length.size())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length(); m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
m_current_height += m_layer_info->height; m_current_height += m_layer_info->height;
@@ -2435,6 +2448,10 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
if (m_plan.empty()) if (m_plan.empty())
return; return;
// Before planning: the layer heights this rewrites feed the extrusion flow of every later pass.
if (m_sparse_layers_combined)
combine_sparse_wipe_tower_plan(m_plan, m_filpar, m_first_layer_idx, m_current_tool);
plan_tower(); plan_tower();
#if 1 #if 1
for (int i=0;i<5;++i) { for (int i=0;i<5;++i) {
@@ -2533,6 +2550,10 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
layer_result[idx] = merge_tcr(layer_result[idx], finish_layer_tcr); layer_result[idx] = merge_tcr(layer_result[idx], finish_layer_tcr);
} }
if (layer.combined_away)
for (WipeTower::ToolChangeResult &tcr : layer_result)
tcr.combined_away = true;
result.emplace_back(std::move(layer_result)); result.emplace_back(std::move(layer_result));
if (m_used_filament_length_until_layer.empty() || m_used_filament_length_until_layer.back().first != layer.z) if (m_used_filament_length_until_layer.empty() || m_used_filament_length_until_layer.back().first != layer.z)
+5
View File
@@ -200,6 +200,8 @@ public:
float tower_interface_pre_extrusion_length = 0.f; float tower_interface_pre_extrusion_length = 0.f;
float tower_ironing_area = 4.f; float tower_ironing_area = 4.f;
float tower_interface_purge_length = 0.f; float tower_interface_purge_length = 0.f;
// Tallest layer this filament's nozzle can lay down; caps the sparse layer combination.
float max_layer_height = 0.f;
}; };
private: private:
@@ -268,6 +270,7 @@ private:
float m_extra_loading_move = 0.f; float m_extra_loading_move = 0.f;
float m_bridging = 0.f; float m_bridging = 0.f;
bool m_sparse_layers_skipped = false; bool m_sparse_layers_skipped = false;
bool m_sparse_layers_combined = false;
bool m_set_extruder_trimpot = false; bool m_set_extruder_trimpot = false;
bool m_adhesion = true; bool m_adhesion = true;
GCodeFlavor m_gcode_flavor; GCodeFlavor m_gcode_flavor;
@@ -368,6 +371,8 @@ private:
float z; // z position of the layer float z; // z position of the layer
float height; // layer height float height; // layer height
float depth; // depth of the layer based on all layers above float depth; // depth of the layer based on all layers above
// Folded into a later, thicker layer, so this one prints nothing at all.
bool combined_away{false};
float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; } float toolchanges_depth() const { float sum = 0.f; for (const auto &a : tool_changes) sum += a.required_depth; return sum; }
std::vector<ToolChange> tool_changes; std::vector<ToolChange> tool_changes;
+37 -12
View File
@@ -2256,13 +2256,45 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
// ORCA: // ORCA:
// Inner Outer Inner wall ordering mode perimeter order optimisation functions // Inner Outer Inner wall ordering mode perimeter order optimisation functions
// Whether two Arachne lines touch: somewhere the gap between their centrelines is no more than the
// touching distance there. Each junction of one line is measured against the segments of the other,
// both ways, and the search stops at the first spot that touches.
// Arachne varies line width to fill the region (e.g. the odd centre line of a narrow wall is wider
// than nominal), so the touching distance is half the combined width at the closest points, not the
// nominal spacing. Widths are taken locally so a line widened in one place (a wedge tip, a wall
// transition) does not count as touching where it passes close to other perimeters. min_threshold keeps
// the nominal spacing threshold as the lower bound.
static bool arachne_lines_touch(const Arachne::ExtrusionLine &a, const Arachne::ExtrusionLine &b, double min_threshold)
{
auto one_way = [min_threshold](const Arachne::ExtrusionLine &from, const Arachne::ExtrusionLine &to) {
for (const Arachne::ExtrusionJunction &j : from.junctions) {
const Vec2d p = j.p.cast<double>();
for (size_t k = 0; k + 1 < to.junctions.size(); ++k) {
const Arachne::ExtrusionJunction &j0 = to.junctions[k];
const Arachne::ExtrusionJunction &j1 = to.junctions[k + 1];
const Vec2d s0 = j0.p.cast<double>();
const Vec2d seg = j1.p.cast<double>() - s0;
const double l2 = seg.squaredNorm();
const double t = l2 > 0. ? std::clamp((p - s0).dot(seg) / l2, 0., 1.) : 0.;
const double w = double(j0.w) + t * double(j1.w - j0.w); // width of `to` at the closest point
const double touch_distance = std::max(min_threshold, 0.5 * (double(j.w) + w));
if ((s0 + t * seg - p).norm() <= touch_distance)
return true;
}
}
return false;
};
return one_way(a, b) || one_way(b, a);
}
/** /**
* @brief Finds all perimeters touching a given set of reference lines, given as indexes. * @brief Finds all perimeters touching a given set of reference lines, given as indexes.
* *
* @param entities The list of PerimeterGeneratorArachneExtrusion entities. * @param entities The list of PerimeterGeneratorArachneExtrusion entities.
* @param referenceIndices A set of indices representing the reference points. * @param referenceIndices A set of indices representing the reference points.
* @param threshold_external The distance threshold to consider for proximity for a reference perimeter with inset index 0 * @param threshold_external The minimum touching distance for a reference perimeter with inset index 0
* @param threshold_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+ * @param threshold_internal The minimum touching distance for a reference perimeter with inset index 1+
* @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc). * @param considered_inset_idx What perimeter inset index are we searching for (eg. if we are searching for first internal perimeters proximate to the current reference perimeter, this value should be set to 1 etc).
* @return std::vector<int> A vector of indices representing the touching perimeters. * @return std::vector<int> A vector of indices representing the touching perimeters.
*/ */
@@ -2271,7 +2303,6 @@ std::vector<int> findAllTouchingPerimeters(const std::vector<PerimeterGeneratorA
for (const int refIdx : referenceIndices) { for (const int refIdx : referenceIndices) {
const auto& referenceEntity = entities[refIdx]; const auto& referenceEntity = entities[refIdx];
Points referencePoints = Arachne::to_points(*referenceEntity.extrusion);
for (size_t i = 0; i < entities.size(); ++i) { for (size_t i = 0; i < entities.size(); ++i) {
// Skip already considered references and the reference entity // Skip already considered references and the reference entity
if (referenceIndices.count(i) > 0) continue; if (referenceIndices.count(i) > 0) continue;
@@ -2282,15 +2313,9 @@ std::vector<int> findAllTouchingPerimeters(const std::vector<PerimeterGeneratorA
continue; // skip if they dont match continue; // skip if they dont match
} }
Points points = Arachne::to_points(*entity.extrusion); // Add to touchingIndices if the lines touch.
double distance = MultiPoint::minimumDistanceBetweenLinesDefinedByPoints(referencePoints, points); const double threshold = double(referenceEntity.extrusion->inset_idx == 0 ? threshold_external : threshold_internal);
// Add to touchingIndices if within threshold distance if (arachne_lines_touch(*referenceEntity.extrusion, *entity.extrusion, threshold)) {
size_t threshold=0;
if(referenceEntity.extrusion->inset_idx == 0)
threshold = threshold_external;
else
threshold = threshold_internal;
if (distance <= threshold) {
touchingIndices.insert(i); touchingIndices.insert(i);
} }
} }
+3
View File
@@ -2,6 +2,7 @@
#define slic3r_PerimeterGenerator_hpp_ #define slic3r_PerimeterGenerator_hpp_
#include "libslic3r.h" #include "libslic3r.h"
#include <optional>
#include <vector> #include <vector>
#include "Layer.hpp" #include "Layer.hpp"
#include "Flow.hpp" #include "Flow.hpp"
@@ -105,6 +106,8 @@ public:
bool has_fuzzy_hole = false; bool has_fuzzy_hole = false;
// Preserve construction order so overlap precedence remains deterministic. // Preserve construction order so overlap precedence remains deterministic.
std::vector<std::pair<FuzzySkinConfig, ExPolygons>> regions_by_fuzzify; std::vector<std::pair<FuzzySkinConfig, ExPolygons>> regions_by_fuzzify;
// Area resting on the layer below, where fuzzy skin is allowed. Unset means no restriction.
std::optional<ExPolygons> fuzzy_supported_area;
PerimeterGenerator( PerimeterGenerator(
// Input: // Input:
+1
View File
@@ -1199,6 +1199,7 @@ static std::vector<std::string> s_Preset_print_options{
"enable_tower_interface_features", "enable_tower_interface_features",
"enable_tower_interface_cooldown_during_tower", "enable_tower_interface_cooldown_during_tower",
"wipe_tower_no_sparse_layers", "wipe_tower_no_sparse_layers",
"wipe_tower_sparse_layers_combination",
"compatible_printers", "compatible_printers",
"compatible_printers_condition", "compatible_printers_condition",
"inherits", "inherits",
+1
View File
@@ -378,6 +378,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "wipe_tower_bridging" || opt_key == "wipe_tower_bridging"
|| opt_key == "wipe_tower_extra_flow" || opt_key == "wipe_tower_extra_flow"
|| opt_key == "wipe_tower_no_sparse_layers" || opt_key == "wipe_tower_no_sparse_layers"
|| opt_key == "wipe_tower_sparse_layers_combination"
|| opt_key == "flush_volumes_matrix" || opt_key == "flush_volumes_matrix"
|| opt_key == "prime_volume" || opt_key == "prime_volume"
|| opt_key == "flush_into_infill" || opt_key == "flush_into_infill"
+14
View File
@@ -6705,6 +6705,20 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced; def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false)); def->set_default_value(new ConfigOptionBool(false));
def = this->add("wipe_tower_sparse_layers_combination", coBool);
def->label = L("Combine sparse layers");
def->tooltip = L("If enabled, consecutive layers on which the prime tower has no filament change are printed as a single "
"thicker tower layer instead of one thin layer each, the same way infill combination merges sparse infill. "
"The merged layer is printed at the top of the run, at the height of everything it covers.\n\n"
"Only whole layers are merged, and never past the maximum layer height of the nozzle printing the tower "
"(three quarters of the nozzle diameter when that is left at 0). Two or more layers therefore have to fit "
"under that limit before anything changes at all: at a 0.2 mm layer height under a 0.3 mm maximum nothing "
"is merged, while at 0.1 mm three layers become one.\n\n"
"Unlike \"No sparse layers\" the tower keeps following the model, so the toolhead never has to reach down to it. "
"Has no effect with \"No sparse layers\", smooth timelapse or clumping detection, which need a tower on every layer.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBool(false));
def = this->add("single_extruder_multi_material_priming", coBool); def = this->add("single_extruder_multi_material_priming", coBool);
def->label = L("Prime all printing extruders"); def->label = L("Prime all printing extruders");
def->tooltip = L("If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print."); def->tooltip = L("If enabled, all printing extruders will be primed at the front edge of the print bed at the start of the print.");
+2
View File
@@ -163,6 +163,7 @@ inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline =
case ipGrid: case ipGrid:
case ipTriangles: case ipTriangles:
case ipStars: case ipStars:
case ipCubic:
return multiline > 1; return multiline > 1;
default: default:
return false; return false;
@@ -1631,6 +1632,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionString, toolchange_cyclic_order)) ((ConfigOptionString, toolchange_cyclic_order))
((ConfigOptionBool, toolchange_cyclic_first_layer)) ((ConfigOptionBool, toolchange_cyclic_first_layer))
((ConfigOptionBool, wipe_tower_no_sparse_layers)) ((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionBool, wipe_tower_sparse_layers_combination))
((ConfigOptionString, change_filament_gcode)) ((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode)) ((ConfigOptionString, change_extrusion_role_gcode))
((ConfigOptionString, process_change_extrusion_role_gcode)) ((ConfigOptionString, process_change_extrusion_role_gcode))
+4 -4
View File
@@ -819,7 +819,7 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) :
m_scrolled_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL); m_scrolled_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL | wxHSCROLL);
m_scrolled_window->SetBackgroundColour(*wxWHITE); m_scrolled_window->SetBackgroundColour(*wxWHITE);
m_scrolled_window->SetScrollRate(0, FromDIP(10)); m_scrolled_window->SetScrollRate(0, FromDIP(20));
wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *title_sizer_h= new wxBoxSizer(wxHORIZONTAL);
wxBoxSizer *title_sizer_v = new wxBoxSizer(wxVERTICAL); wxBoxSizer *title_sizer_v = new wxBoxSizer(wxVERTICAL);
@@ -1000,13 +1000,13 @@ AmsMapingPopup::AmsMapingPopup(wxWindow *parent, bool use_in_sync_dialog) :
show_pos.x = screen_size.GetLeft(); show_pos.x = screen_size.GetLeft();
m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height)); m_scrolled_window->SetMaxSize(wxSize(screen_size.GetWidth(), popup_height));
m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height)); m_scrolled_window->SetMinSize(wxSize(screen_size.GetWidth(), popup_height));
m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(10)); m_scrolled_window->SetScrollRate(FromDIP(10), FromDIP(20));
} }
else else
{ {
m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height)); m_scrolled_window->SetMaxSize(wxSize(popup_width, popup_height));
m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height)); m_scrolled_window->SetMinSize(wxSize(popup_width, popup_height));
m_scrolled_window->SetScrollRate(0, FromDIP(10)); m_scrolled_window->SetScrollRate(0, FromDIP(20));
} }
//Vertical Direction Processing //Vertical Direction Processing
@@ -2117,7 +2117,7 @@ void AmsReplaceMaterialDialog::create()
identical_filament->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // Orca: accent teal (not brand green) identical_filament->SetForegroundColour(StateColor::darkModeColorFor(wxColour("#009688"))); // Orca: accent teal (not brand green)
m_scrollview_groups = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); m_scrollview_groups = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrollview_groups->SetScrollRate(5, 5); m_scrollview_groups->SetScrollRate(5, FromDIP(20));
//m_scrollview_groups->SetMinSize(wxSize(400, 400)); //m_scrollview_groups->SetMinSize(wxSize(400, 400));
//m_scrollview_groups->SetMaxSize(wxSize(400, 400)); //m_scrollview_groups->SetMaxSize(wxSize(400, 400));
m_scrollview_sizer = new wxBoxSizer(wxVERTICAL); m_scrollview_sizer = new wxBoxSizer(wxVERTICAL);
+1 -1
View File
@@ -606,7 +606,7 @@ AuFolderPanel::AuFolderPanel(wxWindow *parent, AuxiliaryFolderType type, wxWindo
wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL); wxBoxSizer *sizer_main = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 5); m_scrolledWindow->SetScrollRate(5, FromDIP(20));
wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL); wxBoxSizer *sizer_body = new wxBoxSizer(wxVERTICAL);
wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *sizer_top = new wxBoxSizer(wxHORIZONTAL);
+1 -1
View File
@@ -558,7 +558,7 @@ PingCodeBindDialog::~PingCodeBindDialog() {
//show bind failed info //show bind failed info
m_sw_bind_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(450), FromDIP(300)), wxVSCROLL); m_sw_bind_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(450), FromDIP(300)), wxVSCROLL);
m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE); m_sw_bind_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_bind_failed_info->SetScrollRate(5, 5); m_sw_bind_failed_info->SetScrollRate(5, FromDIP(20));
m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90))); m_sw_bind_failed_info->SetMinSize(wxSize(FromDIP(450), FromDIP(90)));
m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90))); m_sw_bind_failed_info->SetMaxSize(wxSize(FromDIP(450), FromDIP(90)));
+1 -1
View File
@@ -3521,7 +3521,7 @@ DesignPanel::DesignPanel(wxWindow* parent)
cards->Show(m_box_mate, false, true); cards->Show(m_box_mate, false, true);
m_form->FitInside(); m_form->FitInside();
m_form->SetScrollRate(0, 10); // vertical only, like Prepare's sidebar: never scroll labels out m_form->SetScrollRate(0, FromDIP(20)); // vertical only, like Prepare's sidebar: never scroll labels out
m_form->SetMinSize(wxSize(264, -1)); m_form->SetMinSize(wxSize(264, -1));
// Right column: a small view toolbar over the live 3D viewport that mirrors // Right column: a small view toolbar over the live 3D viewport that mirrors
+1 -1
View File
@@ -115,7 +115,7 @@ HistoryWindow::HistoryWindow(wxWindow* parent, const std::vector<PACalibResult>&
auto main_sizer = new wxBoxSizer(wxVERTICAL); auto main_sizer = new wxBoxSizer(wxVERTICAL);
auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); auto scroll_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
scroll_window->SetScrollRate(5, 5); scroll_window->SetScrollRate(5, FromDIP(20));
scroll_window->SetBackgroundColour(*wxWHITE); scroll_window->SetBackgroundColour(*wxWHITE);
scroll_window->SetMinSize(HISTORY_WINDOW_SIZE); scroll_window->SetMinSize(HISTORY_WINDOW_SIZE);
scroll_window->SetSize(HISTORY_WINDOW_SIZE); scroll_window->SetSize(HISTORY_WINDOW_SIZE);
+1 -1
View File
@@ -207,7 +207,7 @@ SelectMObjectPopup::SelectMObjectPopup(wxWindow* parent)
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE); m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
m_scrolledWindow->SetScrollRate(0, 5); m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y);
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL); auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow); m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
m_scrolledWindow->Layout(); m_scrolledWindow->Layout();
+1 -1
View File
@@ -81,7 +81,7 @@ CalibrationWizard::CalibrationWizard(wxWindow* parent, CalibMode mode, wxWindowI
wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* main_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 5); m_scrolledWindow->SetScrollRate(5, FromDIP(20));
m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetBackgroundColour(*wxWHITE);
wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer* padding_sizer = new wxBoxSizer(wxHORIZONTAL);
+1 -1
View File
@@ -834,7 +834,7 @@ void CaliPageSendingPanel::create(wxWindow* parent)
m_sw_print_failed_info = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL); m_sw_print_failed_info = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL);
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_print_failed_info->SetScrollRate(0, 5); m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
+2
View File
@@ -1046,6 +1046,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive. // Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
toggle_line("wipe_tower_no_sparse_layers", have_prime_tower); toggle_line("wipe_tower_no_sparse_layers", have_prime_tower);
// Dropping the sparse layers outright leaves nothing to combine, so the two are exclusive.
toggle_line("wipe_tower_sparse_layers_combination", have_prime_tower && !config->opt_bool("wipe_tower_no_sparse_layers"));
WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type"); WipeTowerWallType wipe_tower_wall_type = config->opt_enum<WipeTowerWallType>("wipe_tower_wall_type");
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower; bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
+1 -1
View File
@@ -2710,7 +2710,7 @@ ConfigWizard::ConfigWizard(wxWindow *parent)
SetSizerAndFit(vsizer); SetSizerAndFit(vsizer);
// We can now enable scrolling on hscroll // We can now enable scrolling on hscroll
p->hscroll->SetScrollRate(30, 30); p->hscroll->SetScrollRate(30, FromDIP(20));
on_window_geometry(this, [this]() { on_window_geometry(this, [this]() {
p->init_dialog_size(); p->init_dialog_size();
+5 -5
View File
@@ -699,7 +699,7 @@ CreateFilamentPresetDialog::CreateFilamentPresetDialog(wxWindow *parent)
m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY); m_scrolled_preset_panel = new wxScrolledWindow(this, wxID_ANY);
m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350))); m_scrolled_preset_panel->SetMaxSize(wxSize(-1, FromDIP(350)));
m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE); m_scrolled_preset_panel->SetBackgroundColour(*wxWHITE);
m_scrolled_preset_panel->SetScrollRate(5, 5); m_scrolled_preset_panel->SetScrollRate(5, FromDIP(20));
m_scrolled_sizer = new wxBoxSizer(wxVERTICAL); m_scrolled_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolled_sizer->Add(create_item(FilamentOptionType::PRESET_FOR_PRINTER), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5)); m_scrolled_sizer->Add(create_item(FilamentOptionType::PRESET_FOR_PRINTER), 0, wxEXPAND | wxLEFT | wxRIGHT | wxBOTTOM, FromDIP(5));
m_scrolled_sizer->Add(0, 0, 0, wxTOP, FromDIP(5)); m_scrolled_sizer->Add(0, 0, 0, wxTOP, FromDIP(5));
@@ -1593,7 +1593,7 @@ CreatePrinterPresetDialog::CreatePrinterPresetDialog(wxWindow *parent)
m_page1 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page1 = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_page1->SetBackgroundColour(*wxWHITE); m_page1->SetBackgroundColour(*wxWHITE);
m_page1->SetScrollRate(5, 5); m_page1->SetScrollRate(5, FromDIP(20));
m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE); m_page2 = new wxPanel(this, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_page2->SetBackgroundColour(*wxWHITE);
create_printer_page1(m_page1); create_printer_page1(m_page1);
@@ -2652,7 +2652,7 @@ wxBoxSizer *CreatePrinterPresetDialog::create_presets_template_item(wxWindow *pa
wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer *vertical_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolled_preset_window = new wxScrolledWindow(parent); m_scrolled_preset_window = new wxScrolledWindow(parent);
m_scrolled_preset_window->SetScrollRate(5, 5); m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE); m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
//m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1))); //m_scrolled_preset_window->SetMinSize(wxSize(FromDIP(1500), FromDIP(-1)));
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(1500), FromDIP(-1))); m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(1500), FromDIP(-1)));
@@ -4297,7 +4297,7 @@ wxBoxSizer *ExportConfigsDialog::create_select_printer(wxWindow *parent)
optionSizer->SetMinSize(OPTION_SIZE); optionSizer->SetMinSize(OPTION_SIZE);
horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10)); horizontal_sizer->Add(optionSizer, 0, wxEXPAND | wxALL, FromDIP(10));
m_scrolled_preset_window = new wxScrolledWindow(parent); m_scrolled_preset_window = new wxScrolledWindow(parent);
m_scrolled_preset_window->SetScrollRate(5, 5); m_scrolled_preset_window->SetScrollRate(5, FromDIP(20));
m_scrolled_preset_window->SetBackgroundColour(*wxWHITE); m_scrolled_preset_window->SetBackgroundColour(*wxWHITE);
m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400))); m_scrolled_preset_window->SetMaxSize(wxSize(FromDIP(660), FromDIP(400)));
m_scrolled_preset_window->SetSize(wxSize(FromDIP(660), FromDIP(400))); m_scrolled_preset_window->SetSize(wxSize(FromDIP(660), FromDIP(400)));
@@ -4736,7 +4736,7 @@ wxBoxSizer *EditFilamentPresetDialog::create_preset_tree_sizer()
{ {
wxBoxSizer *filament_preset_tree_sizer = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *filament_preset_tree_sizer = new wxBoxSizer(wxHORIZONTAL);
m_preset_tree_window = new wxScrolledWindow(this); m_preset_tree_window = new wxScrolledWindow(this);
m_preset_tree_window->SetScrollRate(5, 5); m_preset_tree_window->SetScrollRate(5, FromDIP(20));
m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR); m_preset_tree_window->SetBackgroundColour(PRINTER_LIST_COLOUR);
m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400))); m_preset_tree_window->SetMinSize(wxSize(-1, FromDIP(400)));
m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300))); m_preset_tree_window->SetMaxSize(wxSize(-1, FromDIP(300)));
+18 -34
View File
@@ -9,8 +9,6 @@
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include <slic3r/GUI/DeviceCore/DevDefs.h>
#include <slic3r/GUI/GUI_App.hpp>
namespace Slic3r namespace Slic3r
{ {
@@ -153,48 +151,34 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
int DevNozzle::GetLogicExtruderId() const int DevNozzle::GetLogicExtruderId() const
{ {
int total_ext_count = GetTotalExtruderCount(); int total_ext_count = GetTotalExtruderCount();
if (total_ext_count == 1) {
if (GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
if (total_ext_count == 1) {
return LOGIC_UNIQUE_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtLeftExtruder()) {
return LOGIC_L_EXTRUDER_ID;
} else if (AtRightExtruder()) {
return LOGIC_R_EXTRUDER_ID;
}
}
assert(0);
return LOGIC_UNIQUE_EXTRUDER_ID; return LOGIC_UNIQUE_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtLeftExtruder()) {
return LOGIC_L_EXTRUDER_ID;
} else if (AtRightExtruder()) {
return LOGIC_R_EXTRUDER_ID;
}
} }
// For some reason, BBL's logical extruder ID is inverted: assert(0);
// physical extruder id = 0 (MAIN_EXTRUDER_ID) vs logical extruder id = 1 (LOGIC_R_EXTRUDER_ID) return LOGIC_UNIQUE_EXTRUDER_ID;
// Likely because logical id reads from left to right (left = 0, right = 1)
// For generic N extruders, this inversion does not apply.
if (IsOnRack()) return INVALID_EXTRUDER_ID;
return m_nozzle_id;
} }
int DevNozzle::GetExtruderId() const int DevNozzle::GetExtruderId() const
{ {
if (GUI::wxGetApp().preset_bundle->is_bbl_vendor()) { int total_ext_count = GetTotalExtruderCount();
int total_ext_count = GetTotalExtruderCount(); if (total_ext_count == 1) {
if (total_ext_count == 1) {
return MAIN_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtRightExtruder()) {
return MAIN_EXTRUDER_ID;
} else if (AtLeftExtruder()) {
return DEPUTY_EXTRUDER_ID;
}
}
return MAIN_EXTRUDER_ID; return MAIN_EXTRUDER_ID;
} else if (total_ext_count == 2) {
if (AtRightExtruder()) {
return MAIN_EXTRUDER_ID;
} else if (AtLeftExtruder()) {
return DEPUTY_EXTRUDER_ID;
}
} }
return m_nozzle_id; return MAIN_EXTRUDER_ID;
} }
bool DevNozzle::AtLeftExtruder() const bool DevNozzle::AtLeftExtruder() const
+1 -1
View File
@@ -39,7 +39,7 @@ DeviceErrorDialog::DeviceErrorDialog(MachineObject* obj, wxWindow* parent, wxWin
m_line_top->SetBackgroundColour(wxColour(166, 169, 170)); m_line_top->SetBackgroundColour(wxColour(166, 169, 170));
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scroll_area->SetScrollRate(0, 5); m_scroll_area->SetScrollRate(0, FromDIP(20));
m_scroll_area->SetBackgroundColour(*wxWHITE); m_scroll_area->SetBackgroundColour(*wxWHITE);
m_scroll_area->SetMinSize(wxSize(FromDIP(320), FromDIP(250))); m_scroll_area->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
+12 -17
View File
@@ -1199,24 +1199,19 @@ int MachineObject::get_bed_temperature_limit()
bool MachineObject::is_filament_installed() bool MachineObject::is_filament_installed()
{ {
// if (m_extder_system->GetTotalExtderCount() > 0) { if (m_extder_system->GetTotalExtderCount() > 0) {
// // right//or single // right//or single
// auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID]; auto ext = m_extder_system->m_extders[MAIN_EXTRUDER_ID];
// if (ext.m_ext_has_filament) { if (ext.m_ext_has_filament) {
// return true;
// }
// }
// /*left*/
// if (m_extder_system->GetTotalExtderCount() > 1) {
// auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
// if (ext.m_ext_has_filament) {
// return true;
// }
// }
for (auto& ext : m_extder_system->m_extders) {
if (ext.m_ext_has_filament)
return true; return true;
}
}
/*left*/
if (m_extder_system->GetTotalExtderCount() > 1) {
auto ext = m_extder_system->m_extders[DEPUTY_EXTRUDER_ID];
if (ext.m_ext_has_filament) {
return true;
}
} }
return false; return false;
} }
+12 -9
View File
@@ -1088,8 +1088,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent); wxDEFINE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent);
@@ -7283,6 +7281,16 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h)
m_last_w = w; m_last_w = w;
m_last_h = h; m_last_h = h;
set_imgui_scaling();
this->request_extra_frame();
// ensures that this canvas is current
_set_current();
}
void GLCanvas3D::set_imgui_scaling()
{
float font_size = wxGetApp().em_unit(); float font_size = wxGetApp().em_unit();
#ifdef _WIN32 #ifdef _WIN32
@@ -7295,15 +7303,10 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h)
#endif #endif
#if ENABLE_RETINA_GL #if ENABLE_RETINA_GL
imgui->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor()); wxGetApp().imgui()->set_scaling(font_size, 1.0f, m_retina_helper->get_scale_factor());
#else #else
imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f); wxGetApp().imgui()->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
#endif #endif
this->request_extra_frame();
// ensures that this canvas is current
_set_current();
} }
BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model, bool include_plates) const BoundingBoxf3 GLCanvas3D::_max_bounding_box(bool include_gizmos, bool include_bed_model, bool include_plates) const
+2 -2
View File
@@ -185,8 +185,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_TAB, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_RESETGIZMOS, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_MOVE_SLIDERS, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_JUMP_TO, wxKeyEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_UNDO, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_REDO, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent); wxDECLARE_EVENT(EVT_GLCANVAS_SWITCH_TO_OBJECT, SimpleEvent);
@@ -1301,6 +1299,8 @@ public:
Vec3d _mouse_to_3d(const Point& mouse_pos, float* z = nullptr); Vec3d _mouse_to_3d(const Point& mouse_pos, float* z = nullptr);
bool make_current_for_postinit(); bool make_current_for_postinit();
// Sizes ImGui's fonts and style for this canvas; the fonts are rebuilt when the size changes.
void set_imgui_scaling();
private: private:
bool _is_shown_on_screen() const; bool _is_shown_on_screen() const;
+3 -2
View File
@@ -456,8 +456,9 @@ void GLModel::init_from(const indexed_triangle_set& its)
data.reserve_vertices(3 * its.indices.size()); data.reserve_vertices(3 * its.indices.size());
data.reserve_indices(3 * its.indices.size()); data.reserve_indices(3 * its.indices.size());
// Read user preference: smooth normals enabled // CLI thumbnail generation can initialize models without a wx application.
const bool smooth_normals_enabled = wxGetApp().app_config != nullptr && wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS); const bool smooth_normals_enabled = wxApp::GetInstance() != nullptr && wxGetApp().app_config != nullptr &&
wxGetApp().app_config->get_bool(SETTING_OPENGL_PHONG_SMOOTH_NORMALS);
if (smooth_normals_enabled) { if (smooth_normals_enabled) {
// Use per-corner smooth normals (via IGL) // Use per-corner smooth normals (via IGL)
+14 -8
View File
@@ -857,7 +857,12 @@ void GUI_App::post_init()
slow_bootup = true; slow_bootup = true;
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", slow bootup, won't render gl here."; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", slow bootup, won't render gl here.";
} }
if (!switch_to_3d) { // Starting on Home, the GL resources load at idle so Home paints first and Prepare is never
// shown.
const bool gl_at_idle = !starts_on_prepare() && is_editor();
if (!switch_to_3d && gl_at_idle) {
plater_->select_view_3D("3D");
} else if (!switch_to_3d) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", begin load_gl_resources"; BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ", begin load_gl_resources";
#ifndef __linux__ #ifndef __linux__
mainframe->Freeze(); mainframe->Freeze();
@@ -865,9 +870,6 @@ void GUI_App::post_init()
plater_->canvas3D()->enable_render(false); plater_->canvas3D()->enable_render(false);
mainframe->select_prepare_for_gl_init(); mainframe->select_prepare_for_gl_init();
plater_->select_view_3D("3D"); plater_->select_view_3D("3D");
// The first render happens before the queued new_project() sets the same view.
plater_->get_camera().select_view("topfront");
plater_->get_camera().requires_zoom_to_bed = true;
//BBS init the opengl resource here //BBS init the opengl resource here
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() || if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
!plater_->canvas3D()->make_current_for_postinit()) { !plater_->canvas3D()->make_current_for_postinit()) {
@@ -905,8 +907,6 @@ void GUI_App::post_init()
} }
if (starts_on_prepare()) if (starts_on_prepare())
mainframe->select_tab(TAB_ID_PREPARE); mainframe->select_tab(TAB_ID_PREPARE);
else if (is_editor())
mainframe->select_tab(TAB_ID_HOME);
#ifndef __linux__ #ifndef __linux__
mainframe->Thaw(); mainframe->Thaw();
#endif #endif
@@ -3423,6 +3423,10 @@ bool GUI_App::on_init_inner()
} }
BOOST_LOG_TRIVIAL(info) << "create the main window"; BOOST_LOG_TRIVIAL(info) << "create the main window";
mainframe = new MainFrame(); mainframe = new MainFrame();
// The first render can happen as soon as the frame is shown, before the queued
// new_project() sets the same view.
plater_->get_camera().select_view("topfront");
plater_->get_camera().requires_zoom_to_bed = true;
if (is_editor()) { if (is_editor()) {
if (starts_on_prepare()) { if (starts_on_prepare()) {
mainframe->select_tab(TAB_ID_PREPARE); mainframe->select_tab(TAB_ID_PREPARE);
@@ -8201,10 +8205,12 @@ int GUI_App::input_idle_ms() const
return int(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_last_input).count()); return int(std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now() - m_last_input).count());
} }
// Every wxCommandEvent claims the user-input category, so only real mouse and key events count. // Every wxCommandEvent claims the user-input category, so only real mouse and key events count,
// plus main window resizes, since a border drag produces no mouse events.
int GUI_App::FilterEvent(wxEvent& event) int GUI_App::FilterEvent(wxEvent& event)
{ {
if (!event.IsCommandEvent() && (event.GetEventCategory() & wxEVT_CATEGORY_USER_INPUT)) if ((!event.IsCommandEvent() && (event.GetEventCategory() & wxEVT_CATEGORY_USER_INPUT)) ||
(event.GetEventType() == wxEVT_SIZE && event.GetEventObject() == mainframe))
m_last_input = std::chrono::steady_clock::now(); m_last_input = std::chrono::steady_clock::now();
return Event_Skip; return Event_Skip;
} }
+1 -1
View File
@@ -389,7 +389,7 @@ public:
bool is_editor() const { return m_app_mode == EAppMode::Editor; } bool is_editor() const { return m_app_mode == EAppMode::Editor; }
bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; } bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; }
bool is_recreating_gui() const { return m_is_recreating_gui; } bool is_recreating_gui() const { return m_is_recreating_gui; }
// Milliseconds since the last mouse or keyboard event the app processed. // Milliseconds since the last mouse or keyboard event the app processed, or main window resize.
int input_idle_ms() const; int input_idle_ms() const;
int FilterEvent(wxEvent& event) override; int FilterEvent(wxEvent& event) override;
// The Preferences "Default page" choice, stored as its index: 0 Home, 1 Prepare. // The Preferences "Default page" choice, stored as its index: 0 Home, 1 Prepare.
+2 -2
View File
@@ -2764,12 +2764,12 @@ ObjectTablePanel::ObjectTablePanel( wxWindow* parent, wxWindowID id, const wxPoi
//m_object_grid->AssignTable(m_object_grid_table); //m_object_grid->AssignTable(m_object_grid_table);
m_side_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(440),FromDIP(480)), wxVSCROLL); m_side_window = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(440),FromDIP(480)), wxVSCROLL);
m_side_window->SetScrollRate( 0, 5 ); m_side_window->SetScrollRate(0, FromDIP(20));
m_page_sizer = new wxBoxSizer(wxVERTICAL); m_page_sizer = new wxBoxSizer(wxVERTICAL);
//m_page_top_sizer = new wxBoxSizer(wxHORIZONTAL); //m_page_top_sizer = new wxBoxSizer(wxHORIZONTAL);
m_side_window->SetBackgroundColour(wxColour(0xff, 0xff, 0xff)); m_side_window->SetBackgroundColour(wxColour(0xff, 0xff, 0xff));
m_side_window->SetSizer(m_page_sizer); m_side_window->SetSizer(m_page_sizer);
m_side_window->SetScrollbars(1, 20, 1, 2); m_side_window->SetScrollbars(1, FromDIP(20), 1, 2);
//m_side_window->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER); //m_side_window->ShowScrollbars(wxSHOW_SB_NEVER, wxSHOW_SB_NEVER);
//m_side_window->EnableScrolling(false, true); //m_side_window->EnableScrolling(false, true);
+3 -1
View File
@@ -247,7 +247,9 @@ int get_dpi_for_window(const wxWindow *window)
const HDC hdc = GetDC(hwnd); const HDC hdc = GetDC(hwnd);
if (hdc == NULL) { return DPI_DEFAULT; } if (hdc == NULL) { return DPI_DEFAULT; }
return GetDeviceCaps(hdc, LOGPIXELSX); const int dpi = GetDeviceCaps(hdc, LOGPIXELSX);
ReleaseDC(hwnd, hdc); // GetDC's handle must be released, unlike GetWindowDC's
return dpi;
} }
#elif defined __linux__ #elif defined __linux__
// TODO // TODO
+1 -1
View File
@@ -171,7 +171,7 @@ HMSPanel::HMSPanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wx
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetScrollRate(5, 5); m_scrolledWindow->SetScrollRate(5, FromDIP(20));
m_top_sizer = new wxBoxSizer(wxVERTICAL); m_top_sizer = new wxBoxSizer(wxVERTICAL);
+11 -6
View File
@@ -3,6 +3,7 @@
#include <chrono> #include <chrono>
#include <boost/log/trivial.hpp> #include <boost/log/trivial.hpp>
#include <wx/evtloop.h>
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
@@ -23,12 +24,9 @@ constexpr int quiet_ms = 500;
// queued meanwhile are handled first; a click waits at most a slice plus the unit that // queued meanwhile are handled first; a click waits at most a slice plus the unit that
// overran it. // overran it.
constexpr int slice_ms = 40; constexpr int slice_ms = 40;
// On GTK a due timer runs ahead of repaints and posted events, so the next slice waits a few ms. // Delay before the next slice; on GTK a due timer runs ahead of repaints and posted events,
#ifdef __WXGTK__ // and wxOSX rejects a 0 ms timer.
constexpr int next_slice_ms = 5; constexpr int next_slice_ms = 5;
#else
constexpr int next_slice_ms = 0;
#endif
// True when unhandled keyboard, button, touch or pen input is queued; only Windows can ask. // True when unhandled keyboard, button, touch or pen input is queued; only Windows can ask.
bool input_pending() bool input_pending()
@@ -42,6 +40,13 @@ bool input_pending()
#endif #endif
} }
// True inside a wxYield(), where a slice would build pages in the middle of the code that yielded.
bool yielding()
{
const wxEventLoopBase* loop = wxEventLoopBase::GetActive();
return loop != nullptr && loop->IsYielding();
}
} // namespace } // namespace
IdleScheduler::IdleScheduler(std::function<int()> input_idle_ms) : m_input_idle_ms(std::move(input_idle_ms)) IdleScheduler::IdleScheduler(std::function<int()> input_idle_ms) : m_input_idle_ms(std::move(input_idle_ms))
@@ -69,7 +74,7 @@ void IdleScheduler::tick()
stop(); stop();
return; return;
} }
if (m_input_idle_ms() < quiet_ms || input_pending()) { if (m_input_idle_ms() < quiet_ms || input_pending() || yielding()) {
start(); start();
return; return;
} }
+14 -5
View File
@@ -159,8 +159,12 @@ void KBShortcutsDialog::fill_pages()
if (wxGetApp().is_editor()) { if (wxGetApp().is_editor()) {
page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, { page(_L("Global"), _L("Available anywhere in the window, even while typing in a text field."), ShortcutContext::Global, {
fixed(Section::Application, { alt, "1-9, 0" }, L("Run a speed dial favorite while the dial is open")), fixed(Section::SpeedDial, { alt, "1-9, 0" }, L("Run favorite 1 to 10")),
fixed(Section::SpeedDial, { ctrl, "B" }, L("Pin or unpin the selected action")),
// wx cycles notebook pages on Ctrl+Tab, which is Cmd+Tab on macOS and never arrives there.
#ifndef __APPLE__
fixed(Section::Application, { ctrl, key(L_CONTEXT("Tab", "Keyboard Shortcut")) }, L("Switch to the next main tab")), fixed(Section::Application, { ctrl, key(L_CONTEXT("Tab", "Keyboard Shortcut")) }, L("Switch to the next main tab")),
#endif
}); });
page(_L("Prepare"), _L("Available while the 3D view on the Prepare tab has focus."), ShortcutContext::Plater, { page(_L("Prepare"), _L("Available while the 3D view on the Prepare tab has focus."), ShortcutContext::Plater, {
@@ -207,7 +211,7 @@ wxPanel* KBShortcutsDialog::create_page(wxWindow* parent, const Page& page)
wxGetApp().UpdateDarkUI(scrollable_panel); wxGetApp().UpdateDarkUI(scrollable_panel);
const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE); const wxColour page_colour = StateColor::darkModeColorFor(*wxWHITE);
scrollable_panel->SetBackgroundColour(page_colour); scrollable_panel->SetBackgroundColour(page_colour);
scrollable_panel->SetScrollRate(0, 20); scrollable_panel->SetScrollRate(0, FromDIP(20));
const int page_width = FromDIP(PAGE_WIDTH); const int page_width = FromDIP(PAGE_WIDTH);
scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450))); scrollable_panel->SetInitialSize(wxSize(page_width, FromDIP(450)));
@@ -454,7 +458,10 @@ ShortcutCaptureDialog::ShortcutCaptureDialog(wxWindow* parent, Shortcut shortcut
capture_sizer->Add(m_chord_label, 0, wxALIGN_CENTER); capture_sizer->Add(m_chord_label, 0, wxALIGN_CENTER);
capture_sizer->AddStretchSpacer(); capture_sizer->AddStretchSpacer();
capture->SetSizer(capture_sizer); capture->SetSizer(capture_sizer);
capture->Bind(wxEVT_KEY_DOWN, &ShortcutCaptureDialog::on_key, this); capture->Layout(); // the box is created at its final size, so nothing resizes it into laying the sizer out
// The hook runs before the window procedure, so Windows does not open its window menu
// over the dialog on Alt+Space.
Bind(wxEVT_CHAR_HOOK, &ShortcutCaptureDialog::on_key, this);
capture->Bind(wxEVT_CHAR, &ShortcutCaptureDialog::on_char, this); capture->Bind(wxEVT_CHAR, &ShortcutCaptureDialog::on_char, this);
capture->Bind(wxEVT_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); }); capture->Bind(wxEVT_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); });
sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20)); sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20));
@@ -531,10 +538,12 @@ void ShortcutCaptureDialog::record(const KeyChord& chord)
m_ok->Enable(false); m_ok->Enable(false);
}; };
const bool global = (shortcut_info(m_shortcut).contexts & context_bit(ShortcutContext::Global)) != 0; const bool global = (shortcut_info(m_shortcut).contexts & context_bit(ShortcutContext::Global)) != 0;
if (global && !chord.is_menu_accelerator()) { if (chord.is_system_shortcut()) {
reject(_L("The system uses this shortcut, so it cannot be assigned."));
} else if (global && !chord.is_menu_accelerator()) {
reject(m_rejection); reject(m_rejection);
} else if (const std::optional<Shortcut> owner = wxGetApp().shortcuts().step_owner(m_shortcut, chord); owner.has_value()) { } else if (const std::optional<Shortcut> owner = wxGetApp().shortcuts().step_owner(m_shortcut, chord); owner.has_value()) {
reject(wxString::Format(_L("Already used as a step of %s."), _(shortcut_info(*owner).name))); reject(wxString::Format(_L("Shift and Ctrl with this key belong to %s and cannot be assigned."), _(shortcut_info(*owner).name)));
} else { } else {
m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord); m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord);
m_status->SetForegroundColour(m_status_colour); m_status->SetForegroundColour(m_status_colour);
+10
View File
@@ -182,6 +182,16 @@ bool KeyChord::is_menu_accelerator() const
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE)); return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
} }
// Only a chord the desktop acts on while still delivering it to the app belongs here.
bool KeyChord::is_system_shortcut() const
{
#ifdef _WIN32
return modifiers == wxMOD_ALT && (key == WXK_F4 || key == WXK_SPACE);
#else
return false;
#endif
}
std::string KeyChord::to_string() const std::string KeyChord::to_string() const
{ {
if (!valid()) if (!valid())
+3
View File
@@ -34,6 +34,9 @@ struct KeyChord
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without // True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
// swallowing typing in text fields. // swallowing typing in text fields.
bool is_menu_accelerator() const; bool is_menu_accelerator() const;
// True for a chord the desktop acts on although the app receives it, so a binding would
// take it from the system.
bool is_system_shortcut() const;
// Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings. // Platform-neutral text ("Ctrl+Shift+S") for persistence and wx accelerator strings.
std::string to_string() const; std::string to_string() const;
+72
View File
@@ -81,6 +81,7 @@
#ifdef __WXGTK__ #ifdef __WXGTK__
#include <gtk/gtk.h> #include <gtk/gtk.h>
#include <wx/glcanvas.h>
#endif // __WXGTK__ #endif // __WXGTK__
#include <slic3r/GUI/CreatePresetsDialog.hpp> #include <slic3r/GUI/CreatePresetsDialog.hpp>
@@ -510,6 +511,9 @@ DPIFrame(NULL, wxID_ANY, "", wxDefaultPosition, wxDefaultSize, BORDERLESS_FRAME_
wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_NOTICE_CHILDE_SIZE_CHANGED)); wxQueueEvent(wxGetApp().plater(), new SimpleEvent(EVT_NOTICE_CHILDE_SIZE_CHANGED));
fit_tab_labels(); // ORCA on resize fit_tab_labels(); // ORCA on resize
// Restarts the idle build so a hidden Prepare page is laid out at the new size.
if (m_prebuild_started)
m_idle.start();
}); });
//BBS //BBS
@@ -4008,13 +4012,81 @@ bool MainFrame::Show(bool show)
return changed; return changed;
} }
bool MainFrame::GLResourcesPrebuild::built() const
{
return m_frame.m_plater != nullptr && m_frame.m_plater->canvas3D()->is_initialized() &&
m_frame.m_plater->get_partplate_list().icon_textures_loaded();
}
bool MainFrame::GLResourcesPrebuild::build_step()
{
GLCanvas3D* canvas = m_frame.m_plater->canvas3D();
#ifdef __WXGTK__
// wx creates a GTK canvas's GL surface when the widget is realized, so the context can be
// made current on it while hidden.
gtk_widget_realize(canvas->get_wxglcanvas()->GetHandle());
#endif
if (!canvas->make_current_for_postinit()) {
// The first render of the canvas loads everything instead.
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": cannot make the GL context current on the hidden canvas";
m_failed = true;
return false;
}
switch (m_step) {
case 0:
m_failed = !wxGetApp().init_opengl();
break;
case 1: {
const Size size = canvas->get_canvas_size();
wxGetApp().imgui()->set_display_size(float(std::max(1, size.get_width())), float(std::max(1, size.get_height())));
canvas->set_imgui_scaling();
// Builds the font atlas without leaving a frame open at the hidden canvas's size.
wxGetApp().imgui()->new_frame();
wxGetApp().imgui()->end_frame();
break;
}
case 2:
// One texture per unit until none remain.
if (m_frame.m_plater->get_partplate_list().load_next_plate_texture())
return true;
break;
case 3:
m_failed = !canvas->init();
break;
default:
// Runs after init(), which sets the color mode the icons are drawn for.
m_frame.m_plater->get_partplate_list().load_icon_textures();
return false;
}
++m_step;
return !m_failed;
}
bool MainFrame::PrepareLayoutPrebuild::built() const
{
// The book lays out the page it shows.
const wxWindow* page = m_frame.m_tabpanel != nullptr ? m_frame.m_tabpanel->GetCurrentPage() : nullptr;
return page == nullptr || page == m_frame.m_plater || page->GetSize() == m_laid_out_size;
}
bool MainFrame::PrepareLayoutPrebuild::build_step()
{
// Sized as the book sizes the page it selects, so the selection finds nothing to lay out.
const wxWindow* page = m_frame.m_tabpanel->GetCurrentPage();
m_laid_out_size = page->GetSize();
m_frame.m_plater->SetSize(page->GetRect());
return false;
}
// A page out of the book stays registered and is passed over; a negative order is never // A page out of the book stays registered and is passed over; a negative order is never
// registered. // registered.
void MainFrame::prebuild_pages_when_idle() void MainFrame::prebuild_pages_when_idle()
{ {
m_idle.clear(); m_idle.clear();
m_idle.add(m_gl_prebuild);
if (m_param_panel) if (m_param_panel)
m_idle.add(m_param_panel->settings_page_prebuild()); m_idle.add(m_param_panel->settings_page_prebuild());
m_idle.add(m_prepare_layout_prebuild);
for (LazyBase* page : m_lazy_pages) for (LazyBase* page : m_lazy_pages)
if (page->prebuild_order() >= 0) if (page->prebuild_order() >= 0)
m_idle.add(*page); m_idle.add(*page);
+32
View File
@@ -140,6 +140,38 @@ class MainFrame : public DPIFrame
wxTimer* m_reset_title_text_colour_timer{ nullptr }; wxTimer* m_reset_title_text_colour_timer{ nullptr };
IdleScheduler m_idle; IdleScheduler m_idle;
bool m_prebuild_started{ false }; bool m_prebuild_started{ false };
// Loads the Prepare canvas's GL resources while its page is hidden.
class GLResourcesPrebuild : public LazyBase
{
public:
explicit GLResourcesPrebuild(MainFrame& frame) : m_frame(frame) {}
const std::string& name() const override { return m_name; }
bool built() const override;
bool pending() const override { return !m_failed && !built(); }
bool build_step() override;
int prebuild_order() const override { return 0; }
private:
MainFrame& m_frame;
std::string m_name{ "gl_resources" };
int m_step{ 0 };
bool m_failed{ false };
} m_gl_prebuild{ *this };
// Lays out the hidden Prepare page at the size the book gives its pages.
class PrepareLayoutPrebuild : public LazyBase
{
public:
explicit PrepareLayoutPrebuild(MainFrame& frame) : m_frame(frame) {}
const std::string& name() const override { return m_name; }
bool built() const override;
bool build_step() override;
int prebuild_order() const override { return 0; }
private:
MainFrame& m_frame;
std::string m_name{ "prepare_layout" };
wxSize m_laid_out_size;
} m_prepare_layout_prebuild{ *this };
// Every LazyPage, in and out of the book; prebuild_pages_when_idle() registers them. // Every LazyPage, in and out of the book; prebuild_pages_when_idle() registers them.
std::vector<LazyBase*> m_lazy_pages; std::vector<LazyBase*> m_lazy_pages;
// The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built. // The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built.
+1 -1
View File
@@ -1125,7 +1125,7 @@ wxBoxSizer* MixedFilamentDialog::create_recommendation_grid()
outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4)); outer->Add(title_sizer, 0, wxEXPAND | wxBOTTOM, FromDIP(4));
m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116))); m_recommendation_scroll = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(-1, FromDIP(116)));
m_recommendation_scroll->SetScrollRate(0, 5); m_recommendation_scroll->SetScrollRate(0, FromDIP(20));
m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8"))); m_recommendation_scroll->SetBackgroundColour(StateColor::darkModeColorFor(wxColour("#F8F8F8")));
m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES); m_recommendation_grid = new wxWrapSizer(wxHORIZONTAL, wxREMOVE_LEADING_SPACES);
+4 -3
View File
@@ -194,9 +194,10 @@ Button* MsgDialog::add_button(wxWindowID btn_id, bool set_focus /*= false*/, con
bd->button = btn; bd->button = btn;
//bd->type = type; //bd->type = type;
mb->id = wxString::Format("%d", m_buttons.size()); // m_buttons.size() is a size_t, which does not match the %d conversion
mb->id = wxString(std::to_string(m_buttons.size()));
mb->buttondata = bd; mb->buttondata = bd;
m_buttons[ wxString::Format("%d", m_buttons.size())] = mb; m_buttons[ wxString(std::to_string(m_buttons.size()))] = mb;
return btn; return btn;
}; };
@@ -398,7 +399,7 @@ static void add_msg_content(wxWindow *parent,
} }
wxScrolledWindow *scrolledWindow = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); wxScrolledWindow *scrolledWindow = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
scrolledWindow->SetBackgroundColour(*wxWHITE); scrolledWindow->SetBackgroundColour(*wxWHITE);
scrolledWindow->SetScrollRate(0, 20); scrolledWindow->SetScrollRate(0, scrolledWindow->FromDIP(20));
scrolledWindow->EnableScrolling(false, true); scrolledWindow->EnableScrolling(false, true);
wxBoxSizer *sizer_scrolled = new wxBoxSizer(wxHORIZONTAL); wxBoxSizer *sizer_scrolled = new wxBoxSizer(wxHORIZONTAL);
Label *wrapped_text = new Label(scrolledWindow, font, msg, LB_AUTO_WRAP, wxSize(info_width, -1)); Label *wrapped_text = new Label(scrolledWindow, font, msg, LB_AUTO_WRAP, wxSize(info_width, -1));
+1 -1
View File
@@ -398,7 +398,7 @@ MultiMachineManagerPage::MultiMachineManagerPage(wxWindow* parent)
m_machine_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_machine_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_machine_list->SetBackgroundColour(*wxWHITE); m_machine_list->SetBackgroundColour(*wxWHITE);
m_machine_list->SetScrollRate(0, 5); m_machine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
m_machine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_machine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
m_machine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_machine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
+1 -1
View File
@@ -323,7 +323,7 @@ MultiMachinePickPage::MultiMachinePickPage(Plater* plater /*= nullptr*/)
scroll_macine_list->SetMinSize(wxSize(FromDIP(400), FromDIP(10 * 30))); scroll_macine_list->SetMinSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
scroll_macine_list->SetMaxSize(wxSize(FromDIP(400), FromDIP(10 * 30))); scroll_macine_list->SetMaxSize(wxSize(FromDIP(400), FromDIP(10 * 30)));
scroll_macine_list->SetBackgroundColour(*wxWHITE); scroll_macine_list->SetBackgroundColour(*wxWHITE);
scroll_macine_list->SetScrollRate(0, 5); scroll_macine_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
sizer_machine_list = new wxBoxSizer(wxVERTICAL); sizer_machine_list = new wxBoxSizer(wxVERTICAL);
scroll_macine_list->SetSizer(sizer_machine_list); scroll_macine_list->SetSizer(sizer_machine_list);
+2 -2
View File
@@ -675,7 +675,7 @@ LocalTaskManagerPage::LocalTaskManagerPage(wxWindow* parent)
m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_task_list->SetBackgroundColour(*wxWHITE); m_task_list->SetBackgroundColour(*wxWHITE);
m_task_list->SetScrollRate(0, 5); m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
@@ -1056,7 +1056,7 @@ CloudTaskManagerPage::CloudTaskManagerPage(wxWindow* parent)
m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize); m_task_list = new wxScrolledWindow(m_main_panel, wxID_ANY, wxDefaultPosition, wxDefaultSize);
m_task_list->SetBackgroundColour(*wxWHITE); m_task_list->SetBackgroundColour(*wxWHITE);
m_task_list->SetScrollRate(0, 5); m_task_list->SetScrollRate(0, FromDIP(DEVICE_ITEM_MAX_HEIGHT));
m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMinSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT))); m_task_list->SetMaxSize(wxSize(FromDIP(CLOUD_TASK_ITEM_MAX_WIDTH), 10 * FromDIP(DEVICE_ITEM_MAX_HEIGHT)));
+1 -1
View File
@@ -355,7 +355,7 @@ ObjColorPanel::ObjColorPanel(wxWindow *parent, Slic3r::ObjDialogInOut &in_out, c
//new color table //new color table
m_scrolledWindow = new wxScrolledWindow(m_page_simple, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(m_page_simple, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetScrollRate(0, 20); m_scrolledWindow->SetScrollRate(0, FromDIP(20));
m_scrolledWindow->EnableScrolling(false, true); m_scrolledWindow->EnableScrolling(false, true);
m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT); m_scrolledWindow->ShowScrollbars(wxScrollbarVisibility::wxSHOW_SB_NEVER, wxScrollbarVisibility::wxSHOW_SB_DEFAULT);
draw_new_table(); draw_new_table();
+1 -1
View File
@@ -112,7 +112,7 @@ public:
bool split_multi_line{false}; bool split_multi_line{false};
bool option_label_at_right{false}; bool option_label_at_right{false};
// BBS: new layout // BBS: new layout
wxWindow * stb; wxWindow * stb{ nullptr };
const wxString icon; const wxString icon;
const wxString title; const wxString title;
bool m_labels_hidden{false}; bool m_labels_hidden{false};
+1 -1
View File
@@ -391,7 +391,7 @@ ParamsPanel::ParamsPanel( wxWindow* parent, wxWindowID id, const wxPoint& pos, c
m_page_sizer = new wxBoxSizer(wxVERTICAL); m_page_sizer = new wxBoxSizer(wxVERTICAL);
m_page_view->SetSizer(m_page_sizer); m_page_view->SetSizer(m_page_sizer);
m_page_view->SetScrollbars(1, 20, 1, 2); m_page_view->SetScrollbars(1, FromDIP(20), 1, 2);
//m_page_view->SetScrollRate( 5, 5 ); //m_page_view->SetScrollRate( 5, 5 );
if (m_mode_region) if (m_mode_region)
+2 -2
View File
@@ -122,8 +122,8 @@ class ParamsPanel : public wxPanel
wxPanel* m_current_tab { nullptr }; wxPanel* m_current_tab { nullptr };
// Builds the selected page's option groups at idle; while no tab is selected yet, // Builds the selected page's option groups at idle and then shows them for the mode;
// its first unit selects the default one. // while no tab is selected yet, its first unit selects the default one.
class SettingsPagePrebuild : public LazyBase class SettingsPagePrebuild : public LazyBase
{ {
public: public:
+126 -124
View File
@@ -799,67 +799,8 @@ void PartPlate::render_logo(bool bottom, bool render_cali)
{ {
if (!m_partplate_list->render_bedtype_logo) { if (!m_partplate_list->render_bedtype_logo) {
// render third-party printer texture logo // render third-party printer texture logo
if (m_partplate_list->m_logo_texture_filename.empty()) { if (!m_partplate_list->load_logo_texture())
m_partplate_list->m_logo_texture.reset();
return; return;
}
//GLTexture* temp_texture = const_cast<GLTexture*>(&m_temp_texture);
if (m_partplate_list->m_logo_texture.get_id() == 0 || m_partplate_list->m_logo_texture.get_source() != m_partplate_list->m_logo_texture_filename) {
m_partplate_list->m_logo_texture.reset();
if (boost::algorithm::iends_with(m_partplate_list->m_logo_texture_filename, ".svg")) {
/*// use higher resolution images if graphic card and opengl version allow
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
if (temp_texture->get_id() == 0 || temp_texture->get_source() != m_texture_filename) {
// generate a temporary lower resolution texture to show while no main texture levels have been compressed
if (!temp_texture->load_from_svg_file(m_texture_filename, false, false, false, max_tex_size / 8)) {
render_default(bottom, false);
return;
}
canvas.request_extra_frame();
}*/
// starts generating the main texture, compression will run asynchronously
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
if (!m_partplate_list->m_logo_texture.load_from_svg_file(m_partplate_list->m_logo_texture_filename, true, true, true, logo_tex_size)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_partplate_list->m_logo_texture_filename;
return;
}
}
else if (boost::algorithm::iends_with(m_partplate_list->m_logo_texture_filename, ".png")) {
// generate a temporary lower resolution texture to show while no main texture levels have been compressed
/* if (temp_texture->get_id() == 0 || temp_texture->get_source() != m_logo_texture_filename) {
if (!temp_texture->load_from_file(m_logo_texture_filename, false, GLTexture::None, false)) {
render_default(bottom, false);
return;
}
canvas.request_extra_frame();
}*/
// starts generating the main texture, compression will run asynchronously
if (!m_partplate_list->m_logo_texture.load_from_file(m_partplate_list->m_logo_texture_filename, true, GLTexture::MultiThreaded, true)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_partplate_list->m_logo_texture_filename;
return;
}
}
else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not load logo texture from %1%, unsupported format") % m_partplate_list->m_logo_texture_filename;
return;
}
}
else if (m_partplate_list->m_logo_texture.unsent_compressed_data_available()) {
// sends to gpu the already available compressed levels of the main texture
m_partplate_list->m_logo_texture.send_compressed_data_to_gpu();
// the temporary texture is not needed anymore, reset it
//if (temp_texture->get_id() != 0)
// temp_texture->reset();
//canvas.request_extra_frame();
}
if (m_logo_triangles.is_initialized()) if (m_logo_triangles.is_initialized())
render_logo_texture(m_partplate_list->m_logo_texture, m_logo_triangles, bottom); render_logo_texture(m_partplate_list->m_logo_texture, m_logo_triangles, bottom);
@@ -4232,6 +4173,7 @@ Vec2d PartPlateList::compute_shape_position(int index, int cols)
//generate icon textures //generate icon textures
void PartPlateList::generate_icon_textures() void PartPlateList::generate_icon_textures()
{ {
m_icon_textures_dark = m_is_dark;
// use higher resolution images if graphic card and opengl version allow // use higher resolution images if graphic card and opengl version allow
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(), icon_size = max_tex_size / 8; GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(), icon_size = max_tex_size / 8;
std::string path = resources_dir() + "/images/"; std::string path = resources_dir() + "/images/";
@@ -4447,6 +4389,9 @@ void PartPlateList::release_icon_textures()
PartPlateList::is_load_bedtype_textures = false; PartPlateList::is_load_bedtype_textures = false;
PartPlateList::is_load_extruder_only_area_textures = false; PartPlateList::is_load_extruder_only_area_textures = false;
PartPlateList::is_load_cali_texture = false; PartPlateList::is_load_cali_texture = false;
m_next_bedtype_texture = 0;
m_next_extruder_only_area_texture = 0;
m_next_cali_texture = 0;
for (int i = 0; i < btCount; i++) { for (int i = 0; i < btCount; i++) {
for (auto& part: bed_texture_info[i].parts) { for (auto& part: bed_texture_info[i].parts) {
if (part.texture) { if (part.texture) {
@@ -6002,12 +5947,7 @@ void PartPlateList::render(const Transform3d& view_matrix, const Transform3d& pr
plate_hover_action = hover_id % PartPlate::GRABBER_COUNT; plate_hover_action = hover_id % PartPlate::GRABBER_COUNT;
} }
static bool last_dark_mode_status = m_is_dark; load_icon_textures();
if (m_is_dark != last_dark_mode_status) {
last_dark_mode_status = m_is_dark;
generate_icon_textures();
} else if(m_del_texture.get_id() == 0)
generate_icon_textures();
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) { for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
int current_index = (*it)->get_index(); int current_index = (*it)->get_index();
if (only_current && (current_index != m_current_plate)) if (only_current && (current_index != m_current_plate))
@@ -6149,6 +6089,8 @@ bool PartPlateList::set_shapes(const Pointfs &shape,
} }
is_load_bedtype_textures = false; //reload textures is_load_bedtype_textures = false; //reload textures
is_load_extruder_only_area_textures = false; // reload textures is_load_extruder_only_area_textures = false; // reload textures
m_next_bedtype_texture = 0;
m_next_extruder_only_area_texture = 0;
calc_bounding_boxes(); calc_bounding_boxes();
update_logo_texture_filename(texture_filename); update_logo_texture_filename(texture_filename);
@@ -7089,53 +7031,120 @@ bool PartPlateList::init_extruder_only_area_info()
return true; return true;
} }
void PartPlateList::load_bedtype_textures() static GLint logo_texture_size()
{ {
if (PartPlateList::is_load_bedtype_textures) return; return std::min<GLint>(OpenGLManager::get_gl_info().get_max_tex_size(), 2048);
init_bed_type_info();
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
for (int i = 0; i < (unsigned int)btCount; ++i) {
for (int j = 0; j < bed_texture_info[i].parts.size(); j++) {
std::string filename = resources_dir() + "/images/" + bed_texture_info[i].parts[j].filename;
if (boost::filesystem::exists(filename)) {
PartPlateList::bed_texture_info[i].parts[j].texture = new GLTexture();
if (!PartPlateList::bed_texture_info[i].parts[j].texture->load_from_svg_file(filename, true, true, true, logo_tex_size)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
}
} else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
}
}
}
PartPlateList::is_load_bedtype_textures = true;
} }
void PartPlateList::load_extruder_only_area_textures() { // Loads the texture of the next untried part across the parts of `infos`, in order, advancing
if (PartPlateList::is_load_extruder_only_area_textures) return; // `next`; false once every part has been tried.
static bool load_next_part_texture(PartPlateList::BedTextureInfo* infos, size_t count, size_t& next, bool compress_and_filter)
{
size_t k = next;
for (size_t i = 0; i < count; ++i) {
if (k >= infos[i].parts.size()) {
k -= infos[i].parts.size();
continue;
}
++next;
PartPlateList::BedTextureInfo::TexturePart& part = infos[i].parts[k];
const std::string filename = resources_dir() + "/images/" + part.filename;
if (boost::filesystem::exists(filename)) {
part.texture = new GLTexture();
if (!part.texture->load_from_svg_file(filename, true, compress_and_filter, compress_and_filter, logo_texture_size()))
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load texture from %1% failed!") % filename;
} else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load texture from %1% failed!") % filename;
}
return true;
}
return false;
}
auto ok = init_extruder_only_area_info(); void PartPlateList::load_bedtype_textures()
if (!ok) { {
while (load_next_bedtype_texture()) {}
}
bool PartPlateList::load_next_bedtype_texture()
{
if (PartPlateList::is_load_bedtype_textures)
return false;
if (m_next_bedtype_texture == 0)
init_bed_type_info();
if (load_next_part_texture(bed_texture_info, btCount, m_next_bedtype_texture, true))
return true;
PartPlateList::is_load_bedtype_textures = true;
return false;
}
bool PartPlateList::load_logo_texture()
{
if (m_logo_texture_filename.empty()) {
m_logo_texture.reset();
return false;
}
if (m_logo_texture.get_id() != 0 && m_logo_texture.get_source() == m_logo_texture_filename) {
if (m_logo_texture.unsent_compressed_data_available())
// sends to gpu the already available compressed levels of the main texture
m_logo_texture.send_compressed_data_to_gpu();
return true;
}
m_logo_texture.reset();
// starts generating the main texture, compression will run asynchronously
if (boost::algorithm::iends_with(m_logo_texture_filename, ".svg")) {
if (!m_logo_texture.load_from_svg_file(m_logo_texture_filename, true, true, true, logo_texture_size())) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename;
return false;
}
}
else if (boost::algorithm::iends_with(m_logo_texture_filename, ".png")) {
if (!m_logo_texture.load_from_file(m_logo_texture_filename, true, GLTexture::MultiThreaded, true)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename;
return false;
}
}
else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not load logo texture from %1%, unsupported format") % m_logo_texture_filename;
return false;
}
return true;
}
void PartPlateList::load_icon_textures()
{
if (!icon_textures_loaded())
generate_icon_textures();
}
bool PartPlateList::load_next_plate_texture()
{
if (!render_bedtype_logo) {
load_logo_texture();
return false;
}
return load_next_bedtype_texture() || load_next_cali_texture() || load_next_extruder_only_area_texture();
}
void PartPlateList::load_extruder_only_area_textures()
{
while (load_next_extruder_only_area_texture()) {}
}
bool PartPlateList::load_next_extruder_only_area_texture()
{
if (PartPlateList::is_load_extruder_only_area_textures)
return false;
if (m_next_extruder_only_area_texture == 0 && !init_extruder_only_area_info()) {
PartPlateList::is_load_extruder_only_area_textures = true; PartPlateList::is_load_extruder_only_area_textures = true;
return; return false;
}
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
for (int i = 0; i < (unsigned int) ExtruderOnlyAreaType::btAreaCount; ++i) {
for (int j = 0; j < extruder_only_area_info[i].parts.size(); j++) {
std::string filename = resources_dir() + "/images/" + extruder_only_area_info[i].parts[j].filename;
if (boost::filesystem::exists(filename)) {
PartPlateList::extruder_only_area_info[i].parts[j].texture = new GLTexture();
if (!PartPlateList::extruder_only_area_info[i].parts[j].texture->load_from_svg_file(filename, true, false, false, logo_tex_size)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
}
} else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
}
}
} }
if (load_next_part_texture(extruder_only_area_info, (size_t) ExtruderOnlyAreaType::btAreaCount, m_next_extruder_only_area_texture, false))
return true;
PartPlateList::is_load_extruder_only_area_textures = true; PartPlateList::is_load_extruder_only_area_textures = true;
return false;
} }
void PartPlateList::init_cali_texture_info() void PartPlateList::init_cali_texture_info()
@@ -7150,26 +7159,19 @@ void PartPlateList::init_cali_texture_info()
void PartPlateList::load_cali_textures() void PartPlateList::load_cali_textures()
{ {
if (PartPlateList::is_load_cali_texture) return; while (load_next_cali_texture()) {}
}
init_cali_texture_info(); bool PartPlateList::load_next_cali_texture()
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size(); {
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048; if (PartPlateList::is_load_cali_texture)
for (int i = 0; i < (unsigned int)btCount; ++i) { return false;
for (int j = 0; j < cali_texture_info.parts.size(); j++) { if (m_next_cali_texture == 0)
std::string filename = resources_dir() + "/images/" + cali_texture_info.parts[j].filename; init_cali_texture_info();
if (boost::filesystem::exists(filename)) { if (load_next_part_texture(&cali_texture_info, 1, m_next_cali_texture, true))
PartPlateList::cali_texture_info.parts[j].texture = new GLTexture(); return true;
if (!PartPlateList::cali_texture_info.parts[j].texture->load_from_svg_file(filename, true, true, true, logo_tex_size)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load cali texture from %1% failed!") % filename;
}
}
else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load cali texture from %1% failed!") % filename;
}
}
}
PartPlateList::is_load_cali_texture = true; PartPlateList::is_load_cali_texture = true;
return false;
} }
void PartPlateList::on_extruder_count_changed(int extruder_count) void PartPlateList::on_extruder_count_changed(int extruder_count)
+21
View File
@@ -644,6 +644,7 @@ class PartPlateList : public ObjectBase
std::string m_hover_tooltip; std::string m_hover_tooltip;
bool m_is_dark = false; bool m_is_dark = false;
bool m_icon_textures_dark = false;
int m_filament_count = 1; int m_filament_count = 1;
@@ -943,12 +944,25 @@ public:
bool calc_extruder_only_area(Rect &left_only_rect, Rect &right_only_rect); bool calc_extruder_only_area(Rect &left_only_rect, Rect &right_only_rect);
void init_bed_type_info(); void init_bed_type_info();
bool init_extruder_only_area_info(); bool init_extruder_only_area_info();
// Each load_*_textures() loads whatever of its set is not loaded yet; each load_next_*()
// loads one texture and returns false once none remain.
void load_bedtype_textures(); void load_bedtype_textures();
bool load_next_bedtype_texture();
void load_extruder_only_area_textures(); void load_extruder_only_area_textures();
bool load_next_extruder_only_area_texture();
// Starts loading the printer's logo texture, or sends the levels compressed since; false when
// there is no logo to draw.
bool load_logo_texture();
void show_cali_texture(bool show = true); void show_cali_texture(bool show = true);
void init_cali_texture_info(); void init_cali_texture_info();
void load_cali_textures(); void load_cali_textures();
bool load_next_cali_texture();
bool icon_textures_loaded() const { return m_del_texture.get_id() != 0 && m_icon_textures_dark == m_is_dark; }
void load_icon_textures();
// Loads the next bed-type, calibration or extruder-area texture, or the logo, which rendering
// otherwise loads on first use; false once none remain.
bool load_next_plate_texture();
void on_extruder_count_changed(int extruder_count); void on_extruder_count_changed(int extruder_count);
@@ -960,6 +974,13 @@ public:
BedTextureInfo bed_texture_info[btCount]; BedTextureInfo bed_texture_info[btCount];
BedTextureInfo cali_texture_info; BedTextureInfo cali_texture_info;
BedTextureInfo extruder_only_area_info[(unsigned char) Slic3r::ExtruderOnlyAreaType::btAreaCount]; BedTextureInfo extruder_only_area_info[(unsigned char) Slic3r::ExtruderOnlyAreaType::btAreaCount];
private:
// The next part to load in each texture set, counted across the set's parts in order; reset
// with the set's is_load_* flag.
size_t m_next_bedtype_texture{ 0 };
size_t m_next_cali_texture{ 0 };
size_t m_next_extruder_only_area_texture{ 0 };
}; };
} // namespace GUI } // namespace GUI
+1 -1
View File
@@ -148,7 +148,7 @@ PartSkipDialog::PartSkipDialog(wxWindow *parent) : DPIDialog(parent, wxID_ANY, _
m_line->SetBackgroundColour(wxColor(238, 238, 238)); m_line->SetBackgroundColour(wxColor(238, 238, 238));
m_list_view = new wxScrolledWindow(m_book_third_panel, wxID_ANY, wxDefaultPosition, wxSize(267, -1), wxHSCROLL | wxVSCROLL); m_list_view = new wxScrolledWindow(m_book_third_panel, wxID_ANY, wxDefaultPosition, wxSize(267, -1), wxHSCROLL | wxVSCROLL);
m_list_view->SetScrollRate(5, 5); m_list_view->SetScrollRate(5, FromDIP(30));
m_list_view->SetMinSize(wxSize(FromDIP(267), FromDIP(378))); m_list_view->SetMinSize(wxSize(FromDIP(267), FromDIP(378)));
m_list_view->SetMaxSize(wxSize(FromDIP(267), FromDIP(378))); m_list_view->SetMaxSize(wxSize(FromDIP(267), FromDIP(378)));
m_list_view->SetBackgroundColour(*wxWHITE); m_list_view->SetBackgroundColour(*wxWHITE);
+2 -4
View File
@@ -3049,8 +3049,7 @@ Sidebar::Sidebar(Plater *parent)
// add filament content // add filament content
p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL ); p->m_panel_filament_content = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL );
p->m_panel_filament_content->SetScrollbars(0, 100, 1, 2); p->m_panel_filament_content->SetScrollRate(0, FromDIP(20));
p->m_panel_filament_content->SetScrollRate(0, 5);
//p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)}); //p->m_panel_filament_content->SetMaxSize(wxSize{-1, FromDIP(174)});
p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255)); p->m_panel_filament_content->SetBackgroundColour(wxColour(255, 255, 255));
@@ -3142,8 +3141,7 @@ Sidebar::Sidebar(Plater *parent)
// 3) Mixed filament rows, in their own scroll area so a long mixed list does not // 3) Mixed filament rows, in their own scroll area so a long mixed list does not
// push the physical filament list off screen. // push the physical filament list off screen.
p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL); p->m_mixed_scroll_area = new wxScrolledWindow(p->m_filament_area_wrapper, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxTAB_TRAVERSAL);
p->m_mixed_scroll_area->SetScrollbars(0, 100, 1, 2); p->m_mixed_scroll_area->SetScrollRate(0, FromDIP(20));
p->m_mixed_scroll_area->SetScrollRate(0, 5);
p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); p->m_mixed_scroll_area->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
{ {
auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL); auto* mix_scroll_sizer = new wxBoxSizer(wxVERTICAL);
+1 -1
View File
@@ -1475,7 +1475,7 @@ void PreferencesDialog::create()
app_config = get_app_config(); app_config = get_app_config();
m_parent = new MyscrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_parent = new MyscrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_parent->SetScrollRate(5, 5); m_parent->SetScrollRate(0, FromDIP(20));
m_parent->SetBackgroundColour(*wxWHITE); m_parent->SetBackgroundColour(*wxWHITE);
m_sizer_body = new wxBoxSizer(wxVERTICAL); m_sizer_body = new wxBoxSizer(wxVERTICAL);
+1 -1
View File
@@ -32,7 +32,7 @@ PrintOptionsDialog::PrintOptionsDialog(wxWindow* parent)
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY); m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
m_scrollwindow->SetScrollRate(0, FromDIP(10)); m_scrollwindow->SetScrollRate(0, FromDIP(20));
m_scrollwindow->SetBackgroundColour(*wxWHITE); m_scrollwindow->SetBackgroundColour(*wxWHITE);
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
+1 -1
View File
@@ -1225,7 +1225,7 @@ size_t PublishSettingsDialog::category_index_for(
} }
category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); category.scroll = new wxScrolledWindow(category.page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
category.scroll->SetScrollRate(0, 10); category.scroll->SetScrollRate(0, FromDIP(20));
category.scroll->SetBackgroundColour(GetBackgroundColour()); category.scroll->SetBackgroundColour(GetBackgroundColour());
category.list_sizer = new wxBoxSizer(wxVERTICAL); category.list_sizer = new wxBoxSizer(wxVERTICAL);
category.scroll->SetSizer(category.list_sizer); category.scroll->SetSizer(category.list_sizer);
+6 -6
View File
@@ -76,7 +76,7 @@ ReleaseNoteDialog::ReleaseNoteDialog(Plater *plater /*= nullptr*/)
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL);
m_vebview_release_note->SetScrollRate(5, 5); m_vebview_release_note->SetScrollRate(5, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_vebview_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430))); m_vebview_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430)));
@@ -142,7 +142,7 @@ UpdatePluginDialog::UpdatePluginDialog(wxWindow* parent /*= nullptr*/)
operation_tips->SetMaxSize(wxSize(FromDIP(260), -1)); operation_tips->SetMaxSize(wxSize(FromDIP(260), -1));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(5, 5); m_vebview_release_note->SetScrollRate(5, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_vebview_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_vebview_release_note->SetMinSize(wxSize(FromDIP(260), FromDIP(150))); m_vebview_release_note->SetMinSize(wxSize(FromDIP(260), FromDIP(150)));
m_vebview_release_note->SetMaxSize(wxSize(FromDIP(260), FromDIP(150))); m_vebview_release_note->SetMaxSize(wxSize(FromDIP(260), FromDIP(150)));
@@ -277,7 +277,7 @@ UpdateVersionDialog::UpdateVersionDialog(wxWindow *parent)
m_simplebook_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_simplebook_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_scrollwindows_release_note = new wxScrolledWindow(m_simplebook_release_note, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL); m_scrollwindows_release_note = new wxScrolledWindow(m_simplebook_release_note, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)), wxVSCROLL);
m_scrollwindows_release_note->SetScrollRate(5, 5); m_scrollwindows_release_note->SetScrollRate(5, FromDIP(20));
m_scrollwindows_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_scrollwindows_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
//webview //webview
@@ -538,7 +538,7 @@ SecondaryCheckDialog::SecondaryCheckDialog(wxWindow* parent, wxWindowID id, cons
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(0, 5); m_vebview_release_note->SetScrollRate(0, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetBackgroundColour(*wxWHITE);
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380))); m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
@@ -823,7 +823,7 @@ PrintErrorDialog::PrintErrorDialog(wxWindow* parent, wxWindowID id, const wxStri
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(5)); m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(5));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(0, 5); m_vebview_release_note->SetScrollRate(0, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetBackgroundColour(*wxWHITE);
m_vebview_release_note->SetMinSize(wxSize(FromDIP(320), FromDIP(250))); m_vebview_release_note->SetMinSize(wxSize(FromDIP(320), FromDIP(250)));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
@@ -1122,7 +1122,7 @@ ConfirmBeforeSendDialog::ConfirmBeforeSendDialog(wxWindow* parent, wxWindowID id
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_vebview_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_vebview_release_note->SetScrollRate(0, 5); m_vebview_release_note->SetScrollRate(0, FromDIP(20));
m_vebview_release_note->SetBackgroundColour(*wxWHITE); m_vebview_release_note->SetBackgroundColour(*wxWHITE);
m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380))); m_vebview_release_note->SetMinSize(wxSize(FromDIP(400), FromDIP(380)));
m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15)); m_sizer_right->Add(m_vebview_release_note, 0, wxEXPAND | wxRIGHT | wxLEFT, FromDIP(15));
+2 -2
View File
@@ -31,7 +31,7 @@ SafetyOptionsDialog::SafetyOptionsDialog(wxWindow* parent)
SetSize(FromDIP(480),FromDIP(320)); SetSize(FromDIP(480),FromDIP(320));
m_scrollwindow = new wxScrolledWindow(this, wxID_ANY); m_scrollwindow = new wxScrolledWindow(this, wxID_ANY);
m_scrollwindow->SetScrollRate(0, FromDIP(10)); m_scrollwindow->SetScrollRate(0, FromDIP(20));
m_scrollwindow->SetBackgroundColour(*wxWHITE); m_scrollwindow->SetBackgroundColour(*wxWHITE);
m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMinSize(wxSize(FromDIP(480), wxDefaultCoord));
m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord)); m_scrollwindow->SetMaxSize(wxSize(FromDIP(480), wxDefaultCoord));
@@ -272,4 +272,4 @@ void SafetyOptionsDialog::show_idel_heating_toast(const wxString &text)
m_idel_heating_toast_timer.StartOnce(3000); m_idel_heating_toast_timer.StartOnce(3000);
} }
}} // namespace Slic3r::GUI }} // namespace Slic3r::GUI
+2 -2
View File
@@ -148,7 +148,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
wxBoxSizer* m_scroll_sizer = new wxBoxSizer(wxVERTICAL); wxBoxSizer* m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
m_scroll_area = new wxScrolledWindow(this); m_scroll_area = new wxScrolledWindow(this);
m_scroll_area->SetScrollRate(0, 20); m_scroll_area->SetScrollRate(0, FromDIP(20));
m_scroll_area->SetBackgroundColour(m_colour_def_color); m_scroll_area->SetBackgroundColour(m_colour_def_color);
m_scroll_area->SetMinSize(wxSize(FromDIP(700), FromDIP(600))); m_scroll_area->SetMinSize(wxSize(FromDIP(700), FromDIP(600)));
m_scroll_area->SetMaxSize(wxSize(FromDIP(700), FromDIP(600))); m_scroll_area->SetMaxSize(wxSize(FromDIP(700), FromDIP(600)));
@@ -707,7 +707,7 @@ SelectMachineDialog::SelectMachineDialog(Plater *plater)
//show bind failed info //show bind failed info
m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)), wxVSCROLL); m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)), wxVSCROLL);
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_print_failed_info->SetScrollRate(0, 5); m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
m_sw_print_failed_info->SetMinSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125))); m_sw_print_failed_info->SetMinSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)));
m_sw_print_failed_info->SetMaxSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(SELECT_MACHINE_DIALOG_SIMBOOK_SIZE2.x, FromDIP(125)));
+1 -1
View File
@@ -327,7 +327,7 @@ SelectMachinePopup::SelectMachinePopup(wxWindow *parent)
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, SELECT_MACHINE_LIST_SIZE, wxHSCROLL | wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE); m_scrolledWindow->SetMinSize(SELECT_MACHINE_LIST_SIZE);
m_scrolledWindow->SetScrollRate(0, 5); m_scrolledWindow->SetScrollRate(0, SELECT_MACHINE_ITEM_SIZE.y);
auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL); auto m_sizxer_scrolledWindow = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow); m_scrolledWindow->SetSizer(m_sizxer_scrolledWindow);
m_scrolledWindow->Layout(); m_scrolledWindow->Layout();
+2 -2
View File
@@ -278,7 +278,7 @@ SendMultiMachinePage::SendMultiMachinePage(Plater* plater)
m_main_scroll = new ScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_main_scroll = new ScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_main_scroll->SetBackgroundColour(*wxWHITE); m_main_scroll->SetBackgroundColour(*wxWHITE);
m_main_scroll->SetScrollRate(5, 5); m_main_scroll->SetScrollRate(5, FromDIP(20));
m_sizer_body = new wxBoxSizer(wxVERTICAL); m_sizer_body = new wxBoxSizer(wxVERTICAL);
m_main_page = create_page(); m_main_page = create_page();
@@ -1353,7 +1353,7 @@ wxPanel* SendMultiMachinePage::create_page()
scroll_macine_list = new wxScrolledWindow(main_page, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(800), FromDIP(300)), wxHSCROLL | wxVSCROLL); scroll_macine_list = new wxScrolledWindow(main_page, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(800), FromDIP(300)), wxHSCROLL | wxVSCROLL);
scroll_macine_list->SetBackgroundColour(*wxWHITE); scroll_macine_list->SetBackgroundColour(*wxWHITE);
scroll_macine_list->SetScrollRate(5, 5); scroll_macine_list->SetScrollRate(5, FromDIP(SEND_ITEM_MAX_HEIGHT));
scroll_macine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT))); scroll_macine_list->SetMinSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT)));
scroll_macine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT))); scroll_macine_list->SetMaxSize(wxSize(FromDIP(DEVICE_ITEM_MAX_WIDTH), 10 * FromDIP(SEND_ITEM_MAX_HEIGHT)));
+1 -1
View File
@@ -384,7 +384,7 @@ SendToPrinterDialog::SendToPrinterDialog(Plater *plater)
//show bind failed info //show bind failed info
m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL); m_sw_print_failed_info = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(380), FromDIP(125)), wxVSCROLL);
m_sw_print_failed_info->SetBackgroundColour(*wxWHITE); m_sw_print_failed_info->SetBackgroundColour(*wxWHITE);
m_sw_print_failed_info->SetScrollRate(0, 5); m_sw_print_failed_info->SetScrollRate(0, FromDIP(20));
m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMinSize(wxSize(FromDIP(380), FromDIP(125)));
m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125))); m_sw_print_failed_info->SetMaxSize(wxSize(FromDIP(380), FromDIP(125)));
+4 -1
View File
@@ -147,9 +147,11 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
SHORTCUT(Search, "search", L("Search"), GLOBAL, { 'F', CTRL }), SHORTCUT(Search, "search", L("Search"), GLOBAL, { 'F', CTRL }),
SHORTCUT(SwitchView, "switch_view", L("Switch between Prepare/Preview"), CANVAS, { WXK_TAB }), SHORTCUT(SwitchView, "switch_view", L("Switch between Prepare/Preview"), CANVAS, { WXK_TAB }),
SHORTCUT(CollapseSidebar, "collapse_sidebar", L("Collapse/Expand the sidebar"), CANVAS, { WXK_TAB, SHIFT }), SHORTCUT(CollapseSidebar, "collapse_sidebar", L("Collapse/Expand the sidebar"), CANVAS, { WXK_TAB, SHIFT }),
SHORTCUT(SpeedDial, "speed_dial", L("Open the speed dial"), GLOBAL, { WXK_SPACE }),
SHORTCUT(ReloadDevicePage, "reload_device_page", L("Reload the device page"), CANVAS, { WXK_F5 }), SHORTCUT(ReloadDevicePage, "reload_device_page", L("Reload the device page"), CANVAS, { WXK_F5 }),
SHORTCUT(KeyboardShortcuts, "keyboard_shortcuts", L("Show keyboard shortcuts list"), CANVAS, { '?' }), SHORTCUT(KeyboardShortcuts, "keyboard_shortcuts", L("Show keyboard shortcuts list"), CANVAS, { '?' }),
// Speed Dial
SHORTCUT(SpeedDial, "speed_dial", L("Open the Speed Dial"), GLOBAL, { WXK_SPACE }),
}}; }};
#undef SHORTCUT #undef SHORTCUT
@@ -177,6 +179,7 @@ constexpr std::array<SectionInfo, size_t(ShortcutSection::Count)> section_table
{ Shortcut::ViewDefault, L("Camera") }, { Shortcut::ViewDefault, L("Camera") },
{ Shortcut::ShowLabels, L("Display") }, { Shortcut::ShowLabels, L("Display") },
{ Shortcut::Preferences, L("Application") }, { Shortcut::Preferences, L("Application") },
{ Shortcut::SpeedDial, L("Speed Dial") },
}}; }};
constexpr bool sections_follow_table_order() constexpr bool sections_follow_table_order()
+4 -2
View File
@@ -47,7 +47,9 @@ enum class Shortcut : uint8_t {
// Display // Display
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode, ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
// Application // Application
Preferences, Search, SwitchView, CollapseSidebar, SpeedDial, ReloadDevicePage, KeyboardShortcuts, Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts,
// Speed Dial
SpeedDial,
Count Count
}; };
@@ -66,7 +68,7 @@ struct ShortcutInfo
// Headings of the shortcuts dialog, in listing order. // Headings of the shortcuts dialog, in listing order.
enum class ShortcutSection : uint8_t { enum class ShortcutSection : uint8_t {
Project, SlicingAndPrinting, Selection, Editing, Objects, Placement, Gizmos, Sliders, PaintingTools, Camera, Display, Application, Project, SlicingAndPrinting, Selection, Editing, Objects, Placement, Gizmos, Sliders, PaintingTools, Camera, Display, Application, SpeedDial,
Count Count
}; };
+168 -345
View File
@@ -1302,7 +1302,7 @@ void PrintingTaskPanel::set_star_count(int star_count)
StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name) StatusBasePanel::StatusBasePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos, const wxSize &size, long style, const wxString &name)
: wxScrolledWindow(parent, id, pos, size, wxHSCROLL | wxVSCROLL) : wxScrolledWindow(parent, id, pos, size, wxHSCROLL | wxVSCROLL)
{ {
this->SetScrollRate(25, 25); SetScrollRate(25, FromDIP(20));
Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager(); Slic3r::DeviceManager* dev = Slic3r::GUI::wxGetApp().getDeviceManager();
if (!dev) return; if (!dev) return;
obj = dev->get_selected_machine(); obj = dev->get_selected_machine();
@@ -1875,25 +1875,13 @@ wxBoxSizer *StatusBasePanel::create_misc_control(wxWindow *parent)
void StatusBasePanel::reset_temp_misc_control() void StatusBasePanel::reset_temp_misc_control()
{ {
// reset temp string // reset temp string
if (wxGetApp().preset_bundle->is_bbl_vendor()) { m_tempCtrl_nozzle->SetIconNormal();
m_tempCtrl_nozzle->SetIconNormal(); m_tempCtrl_nozzle->SetLabel(TEMP_BLANK_STR);
m_tempCtrl_nozzle->SetLabel(TEMP_BLANK_STR); m_tempCtrl_nozzle->GetTextCtrl()->SetValue(TEMP_BLANK_STR);
m_tempCtrl_nozzle->GetTextCtrl()->SetValue(TEMP_BLANK_STR);
m_tempCtrl_nozzle_deputy->SetIconNormal(); m_tempCtrl_nozzle_deputy->SetIconNormal();
m_tempCtrl_nozzle_deputy->SetLabel(TEMP_BLANK_STR); m_tempCtrl_nozzle_deputy->SetLabel(TEMP_BLANK_STR);
m_tempCtrl_nozzle_deputy->GetTextCtrl()->SetValue(TEMP_BLANK_STR); m_tempCtrl_nozzle_deputy->GetTextCtrl()->SetValue(TEMP_BLANK_STR);
m_tempCtrl_nozzle->Enable(true);
m_tempCtrl_nozzle_deputy->Enable(true);
} else {
for (auto& tempCtrl : m_tempCtrl_nozzles) {
tempCtrl->SetIconNormal();
tempCtrl->SetLabel(TEMP_BLANK_STR);
tempCtrl->GetTextCtrl()->SetValue(TEMP_BLANK_STR);
tempCtrl->Enable(true);
}
}
m_tempCtrl_bed->SetIconNormal(); m_tempCtrl_bed->SetIconNormal();
m_tempCtrl_bed->SetLabel(TEMP_BLANK_STR); m_tempCtrl_bed->SetLabel(TEMP_BLANK_STR);
@@ -1903,6 +1891,8 @@ void StatusBasePanel::reset_temp_misc_control()
m_tempCtrl_chamber->SetLabel(TEMP_BLANK_STR); m_tempCtrl_chamber->SetLabel(TEMP_BLANK_STR);
m_tempCtrl_chamber->GetTextCtrl()->SetValue(TEMP_BLANK_STR); m_tempCtrl_chamber->GetTextCtrl()->SetValue(TEMP_BLANK_STR);
m_tempCtrl_nozzle->Enable(true);
m_tempCtrl_nozzle_deputy->Enable(true);
m_tempCtrl_chamber->Enable(true); m_tempCtrl_chamber->Enable(true);
m_tempCtrl_bed->Enable(true); m_tempCtrl_bed->Enable(true);
@@ -2473,26 +2463,12 @@ void StatusPanel::wire_controls()
if (id == m_tempCtrl_bed->GetType()) { if (id == m_tempCtrl_bed->GetType()) {
on_set_bed_temp(); on_set_bed_temp();
} else if (id == m_tempCtrl_nozzle->GetType()) { } else if (id == m_tempCtrl_nozzle->GetType()) {
// NOTE: this check assumes any future m_tempCtrl_nozzles population constructs those if (e.GetString() == wxString::Format("%d", MAIN_EXTRUDER_ID)) {
// TempInput widgets with the same "type" id as m_tempCtrl_nozzle (as m_tempCtrl_nozzle_deputy on_set_nozzle_temp(MAIN_EXTRUDER_ID);
// already does today), so their events route into this branch too. } else if (e.GetString() == wxString::Format("%d", DEPUTY_EXTRUDER_ID)) {
if (wxGetApp().preset_bundle->is_bbl_vendor()) { on_set_nozzle_temp(DEPUTY_EXTRUDER_ID);
if (e.GetString() == wxString::Format("%d", MAIN_EXTRUDER_ID)) {
on_set_nozzle_temp(MAIN_EXTRUDER_ID);
} else if (e.GetString() == wxString::Format("%d", DEPUTY_EXTRUDER_ID)) {
on_set_nozzle_temp(DEPUTY_EXTRUDER_ID);
} else {
on_set_nozzle_temp(UNIQUE_EXTRUDER_ID);//there is only one nozzle
}
} else { } else {
// N-extruder widgets all share TEMP_OF_NORMAL_TYPE, so e.GetString() can't on_set_nozzle_temp(UNIQUE_EXTRUDER_ID);//there is only one nozzle
// disambiguate them; identify the one that fired by object identity instead.
for (size_t i = 0; i < m_tempCtrl_nozzles.size(); ++i) {
if (m_tempCtrl_nozzles[i] == e.GetEventObject()) {
on_set_nozzle_temp((int)i);
break;
}
}
} }
} else if (id == m_tempCtrl_chamber->GetType()) { } else if (id == m_tempCtrl_chamber->GetType()) {
if (!m_tempCtrl_chamber->IsOnChanging()) { if (!m_tempCtrl_chamber->IsOnChanging()) {
@@ -2517,17 +2493,10 @@ void StatusPanel::wire_controls()
m_setting_button->Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); m_setting_button->Connect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this);
m_tempCtrl_bed->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_kill_focus), NULL, this); m_tempCtrl_bed->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_kill_focus), NULL, this);
m_tempCtrl_bed->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_set_focus), NULL, this); m_tempCtrl_bed->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_set_focus), NULL, this);
if (wxGetApp().preset_bundle->is_bbl_vendor()) { m_tempCtrl_nozzle->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this);
m_tempCtrl_nozzle->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this); m_tempCtrl_nozzle->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
m_tempCtrl_nozzle->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this); m_tempCtrl_nozzle_deputy->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this);
m_tempCtrl_nozzle_deputy->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this); m_tempCtrl_nozzle_deputy->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
m_tempCtrl_nozzle_deputy->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
} else {
for (auto& tempCtrl : m_tempCtrl_nozzles) {
tempCtrl->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this);
tempCtrl->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
}
}
m_tempCtrl_chamber->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_cham_temp_kill_focus), NULL, this); m_tempCtrl_chamber->Connect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_cham_temp_kill_focus), NULL, this);
m_tempCtrl_chamber->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_cham_temp_set_focus), NULL, this); m_tempCtrl_chamber->Connect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_cham_temp_set_focus), NULL, this);
m_switch_lamp->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_lamp_switch), NULL, this); m_switch_lamp->Connect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_lamp_switch), NULL, this);
@@ -2588,19 +2557,11 @@ StatusPanel::~StatusPanel()
m_setting_button->Disconnect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this); m_setting_button->Disconnect(wxEVT_LEFT_DCLICK, wxMouseEventHandler(StatusPanel::on_camera_enter), NULL, this);
m_tempCtrl_bed->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_kill_focus), NULL, this); m_tempCtrl_bed->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_kill_focus), NULL, this);
m_tempCtrl_bed->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_set_focus), NULL, this); m_tempCtrl_bed->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_bed_temp_set_focus), NULL, this);
m_tempCtrl_nozzle->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this);
m_tempCtrl_nozzle->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
if (wxGetApp().preset_bundle->is_bbl_vendor()) { m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this);
m_tempCtrl_nozzle->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this); m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
m_tempCtrl_nozzle->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this);
m_tempCtrl_nozzle_deputy->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
} else {
for (auto& tempCtrl : m_tempCtrl_nozzles) {
tempCtrl->Disconnect(wxEVT_KILL_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_kill_focus), NULL, this);
tempCtrl->Disconnect(wxEVT_SET_FOCUS, wxFocusEventHandler(StatusPanel::on_nozzle_temp_set_focus), NULL, this);
}
}
m_switch_lamp->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_lamp_switch), NULL, this); m_switch_lamp->Disconnect(wxEVT_COMMAND_BUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_lamp_switch), NULL, this);
/*m_switch_nozzle_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this); /*m_switch_nozzle_fan->Disconnect(wxEVT_COMMAND_TOGGLEBUTTON_CLICKED, wxCommandEventHandler(StatusPanel::on_nozzle_fan_switch), NULL, this);
@@ -3070,14 +3031,8 @@ void StatusPanel::show_printing_status(bool ctrl_area, bool temp_area)
} }
if (!temp_area) { if (!temp_area) {
if (wxGetApp().preset_bundle->is_bbl_vendor()) { m_tempCtrl_nozzle->Enable(false);
m_tempCtrl_nozzle->Enable(false); m_tempCtrl_nozzle_deputy->Enable(false);
m_tempCtrl_nozzle_deputy->Enable(false);
} else {
for (auto& tempCtrl : m_tempCtrl_nozzles)
tempCtrl->Enable(false);
}
m_tempCtrl_bed->Enable(false); m_tempCtrl_bed->Enable(false);
m_tempCtrl_chamber->Enable(false); m_tempCtrl_chamber->Enable(false);
m_switch_speed->Enable(false); m_switch_speed->Enable(false);
@@ -3088,14 +3043,8 @@ void StatusPanel::show_printing_status(bool ctrl_area, bool temp_area)
m_switch_cham_fan->Enable(false);*/ m_switch_cham_fan->Enable(false);*/
m_switch_fan->Enable(false); m_switch_fan->Enable(false);
} else { } else {
if (wxGetApp().preset_bundle->is_bbl_vendor()) { m_tempCtrl_nozzle->Enable();
m_tempCtrl_nozzle->Enable(); m_tempCtrl_nozzle_deputy->Enable();
m_tempCtrl_nozzle_deputy->Enable();
} else {
for (auto& tempCtrl : m_tempCtrl_nozzles)
tempCtrl->Enable();
}
m_tempCtrl_bed->Enable(); m_tempCtrl_bed->Enable();
m_tempCtrl_chamber->Enable(); m_tempCtrl_chamber->Enable();
m_switch_speed->Enable(); m_switch_speed->Enable();
@@ -3124,18 +3073,11 @@ void StatusPanel::update_temp_ctrl(MachineObject *obj)
m_tempCtrl_bed->SetMaxTemp(limit); m_tempCtrl_bed->SetMaxTemp(limit);
if (obj->nozzle_temp_range.size() >= 2) { if (obj->nozzle_temp_range.size() >= 2) {
if (wxGetApp().preset_bundle->is_bbl_vendor()) { m_tempCtrl_nozzle->SetMinTemp(obj->nozzle_temp_range[0]);
m_tempCtrl_nozzle->SetMinTemp(obj->nozzle_temp_range[0]); m_tempCtrl_nozzle->SetMaxTemp(obj->nozzle_temp_range[1]);
m_tempCtrl_nozzle->SetMaxTemp(obj->nozzle_temp_range[1]);
m_tempCtrl_nozzle_deputy->SetMinTemp(obj->nozzle_temp_range[0]); m_tempCtrl_nozzle_deputy->SetMinTemp(obj->nozzle_temp_range[0]);
m_tempCtrl_nozzle_deputy->SetMaxTemp(obj->nozzle_temp_range[1]); m_tempCtrl_nozzle_deputy->SetMaxTemp(obj->nozzle_temp_range[1]);
} else {
for (auto& tempCtrl : m_tempCtrl_nozzles) {
tempCtrl->SetMinTemp(obj->nozzle_temp_range[0]);
tempCtrl->SetMaxTemp(obj->nozzle_temp_range[1]);
}
}
} }
// update temprature if not input temp target // update temprature if not input temp target
@@ -3153,118 +3095,76 @@ void StatusPanel::update_temp_ctrl(MachineObject *obj)
bool to_update_layout = false; bool to_update_layout = false;
int nozzle_num = obj->GetExtderSystem()->GetTotalExtderCount(); int nozzle_num = obj->GetExtderSystem()->GetTotalExtderCount();
if (nozzle_num == 1)
if (wxGetApp().preset_bundle->is_bbl_vendor()) {
if (nozzle_num == 1)
{
m_tempCtrl_nozzle->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(MAIN_EXTRUDER_ID));
m_tempCtrl_nozzle->SetCurrType(TEMP_OF_NORMAL_TYPE);
m_tempCtrl_nozzle_deputy->SetCurrType(TEMP_OF_NORMAL_TYPE);
m_tempCtrl_nozzle_deputy->SetLabel(TEMP_BLANK_STR);
m_tempCtrl_nozzle_deputy->Hide();
if (m_tempCtrl_nozzle->GetMinSize() != TEMP_CTRL_MIN_SIZE_ALIGN_ONE_ICON)
{
to_update_layout = true;
m_tempCtrl_nozzle->SetMinSize(TEMP_CTRL_MIN_SIZE_ALIGN_ONE_ICON);
}
}
else if (nozzle_num == 2)
{
m_tempCtrl_nozzle->SetCurrType(TEMP_OF_MAIN_NOZZLE_TYPE);
m_tempCtrl_nozzle->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(MAIN_EXTRUDER_ID));
m_tempCtrl_nozzle->Show();
m_tempCtrl_nozzle_deputy->SetCurrType(TEMP_OF_DEPUTY_NOZZLE_TYPE);
m_tempCtrl_nozzle_deputy->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(DEPUTY_EXTRUDER_ID));
m_tempCtrl_nozzle_deputy->Show();
if (m_tempCtrl_nozzle->GetMinSize() != TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON)
{
to_update_layout = true;
m_tempCtrl_nozzle->SetMinSize(TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON);
}
}
}
else
{ {
m_tempCtrl_nozzle->Hide(); m_tempCtrl_nozzle->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(MAIN_EXTRUDER_ID));
m_tempCtrl_nozzle->SetCurrType(TEMP_OF_NORMAL_TYPE);
m_tempCtrl_nozzle_deputy->SetCurrType(TEMP_OF_NORMAL_TYPE);
m_tempCtrl_nozzle_deputy->SetLabel(TEMP_BLANK_STR);
m_tempCtrl_nozzle_deputy->Hide(); m_tempCtrl_nozzle_deputy->Hide();
for (size_t i = 0; i < m_tempCtrl_nozzles.size(); ++i) if (m_tempCtrl_nozzle->GetMinSize() != TEMP_CTRL_MIN_SIZE_ALIGN_ONE_ICON)
{ {
m_tempCtrl_nozzles[i]->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent((int)i)); to_update_layout = true;
m_tempCtrl_nozzles[i]->SetCurrType(TEMP_OF_NORMAL_TYPE); m_tempCtrl_nozzle->SetMinSize(TEMP_CTRL_MIN_SIZE_ALIGN_ONE_ICON);
m_tempCtrl_nozzles[i]->Show(); }
}
else if (nozzle_num == 2)
{
m_tempCtrl_nozzle->SetCurrType(TEMP_OF_MAIN_NOZZLE_TYPE);
m_tempCtrl_nozzle->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(MAIN_EXTRUDER_ID));
m_tempCtrl_nozzle->Show();
m_tempCtrl_nozzle_deputy->SetCurrType(TEMP_OF_DEPUTY_NOZZLE_TYPE);
m_tempCtrl_nozzle_deputy->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(DEPUTY_EXTRUDER_ID));
m_tempCtrl_nozzle_deputy->Show();
if (m_tempCtrl_nozzle->GetMinSize() != TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON)
{
to_update_layout = true;
m_tempCtrl_nozzle->SetMinSize(TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON);
} }
} }
if (wxGetApp().preset_bundle->is_bbl_vendor()) { if (m_temp_nozzle_timeout > 0) {
if (m_temp_nozzle_timeout > 0) { m_temp_nozzle_timeout--;
m_temp_nozzle_timeout--; } else {
} else { if (!nozzle_temp_input) {
if (!nozzle_temp_input) { auto main_extder = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID);
auto main_extder = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); if (main_extder)
if (main_extder) {
m_tempCtrl_nozzle->SetTagTemp(main_extder->GetTargetTemp());
m_tempCtrl_nozzle->SetCurrTemp((int)main_extder->GetCurrentTemp());
if (main_extder->GetTargetTemp() - main_extder->GetCurrentTemp() > TEMP_THRESHOLD_VAL)
{ {
m_tempCtrl_nozzle->SetTagTemp(main_extder->GetTargetTemp()); m_tempCtrl_nozzle->SetIconActive();
m_tempCtrl_nozzle->SetCurrTemp((int)main_extder->GetCurrentTemp());
if (main_extder->GetTargetTemp() - main_extder->GetCurrentTemp() > TEMP_THRESHOLD_VAL)
{
m_tempCtrl_nozzle->SetIconActive();
}
else
{
m_tempCtrl_nozzle->SetIconNormal();
}
} }
} else
}
if (m_temp_nozzle_deputy_timeout > 0) {
m_temp_nozzle_deputy_timeout--;
}
else {
if (!nozzle_temp_input && nozzle_num >= 2) {
auto deputy_extder = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID);
if (deputy_extder)
{ {
m_tempCtrl_nozzle_deputy->SetTagTemp(deputy_extder->GetTargetTemp()); m_tempCtrl_nozzle->SetIconNormal();
m_tempCtrl_nozzle_deputy->SetCurrTemp((int)deputy_extder->GetCurrentTemp());
if (deputy_extder->GetTargetTemp() - deputy_extder->GetCurrentTemp() > TEMP_THRESHOLD_VAL)
{
m_tempCtrl_nozzle_deputy->SetIconActive();
}
else
{
m_tempCtrl_nozzle_deputy->SetIconNormal();
}
} }
} }
} }
} }
if (m_temp_nozzle_deputy_timeout > 0) {
m_temp_nozzle_deputy_timeout--;
}
else { else {
assert(m_temp_nozzle_timeouts.size() == m_tempCtrl_nozzles.size()); if (!nozzle_temp_input && nozzle_num >= 2) {
for (size_t i = 0; i < m_temp_nozzle_timeouts.size(); ++i) { auto deputy_extder = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID);
if (m_temp_nozzle_timeouts[i] > 0) { if (deputy_extder)
m_temp_nozzle_timeouts[i]--; {
} else { m_tempCtrl_nozzle_deputy->SetTagTemp(deputy_extder->GetTargetTemp());
if (!nozzle_temp_input) { m_tempCtrl_nozzle_deputy->SetCurrTemp((int)deputy_extder->GetCurrentTemp());
auto extder = obj->GetExtderSystem()->GetExtderById(i); if (deputy_extder->GetTargetTemp() - deputy_extder->GetCurrentTemp() > TEMP_THRESHOLD_VAL)
if (extder) {
{ m_tempCtrl_nozzle_deputy->SetIconActive();
m_tempCtrl_nozzles[i]->SetTagTemp(extder->GetTargetTemp()); }
m_tempCtrl_nozzles[i]->SetCurrTemp((int)extder->GetCurrentTemp()); else
if (extder->GetTargetTemp() - extder->GetCurrentTemp() > TEMP_THRESHOLD_VAL) {
{ m_tempCtrl_nozzle_deputy->SetIconNormal();
m_tempCtrl_nozzles[i]->SetIconActive();
}
else
{
m_tempCtrl_nozzles[i]->SetIconNormal();
}
}
} }
} }
} }
@@ -3337,15 +3237,10 @@ void StatusPanel::update_misc_ctrl(MachineObject *obj)
/*extder*/ /*extder*/
auto extder_system = obj->GetExtderSystem(); auto extder_system = obj->GetExtderSystem();
m_nozzle_num = extder_system->GetTotalExtderCount(); m_nozzle_num = extder_system->GetTotalExtderCount();
// m_extruderImage/m_extruder_book only have dedicated pages for 1 and 2 nozzles (see int select_index = m_nozzle_num - 1;
// create_extruder_control()); 3+ reuses the 2-nozzle page and style, which is a cosmetic
// fallback only (ExtruderImage::doRender's own "m_nozzle_num >= 2" branch already draws the
// same two-icon graphic for any count >= 2) -- this just avoids indexing past the vector's end.
int display_num = m_nozzle_num < (int)m_extruderImage.size() ? m_nozzle_num : (int)m_extruderImage.size();
int select_index = display_num - 1;
if (m_nozzle_num >= 2) { if (m_nozzle_num >= 2) {
m_extruder_book->SetSelection(display_num); m_extruder_book->SetSelection(m_nozzle_num);
/*style*/ /*style*/
m_nozzle_btn_panel->Show(); m_nozzle_btn_panel->Show();
@@ -3391,7 +3286,7 @@ void StatusPanel::update_misc_ctrl(MachineObject *obj)
} }
} else { } else {
m_nozzle_btn_panel->Hide(); m_nozzle_btn_panel->Hide();
m_extruder_book->SetSelection(display_num); m_extruder_book->SetSelection(m_nozzle_num);
m_extruderImage[select_index]->setExtruderCount(m_nozzle_num); m_extruderImage[select_index]->setExtruderCount(m_nozzle_num);
if (extder_system->GetTotalExtderSize() > 0) if (extder_system->GetTotalExtderSize() > 0)
@@ -3578,50 +3473,34 @@ void StatusPanel::update_ams(MachineObject *obj)
// m_ams_control->SetExtruder(obj->is_filament_at_extruder(), obj->m_extder_data.extders[MAIN_NOZZLE_ID].snow.ams_id, obj->m_extder_data.extders[MAIN_NOZZLE_ID].snow.slot_id); // m_ams_control->SetExtruder(obj->is_filament_at_extruder(), obj->m_extder_data.extders[MAIN_NOZZLE_ID].snow.ams_id, obj->m_extder_data.extders[MAIN_NOZZLE_ID].snow.slot_id);
//} else { //} else {
if (wxGetApp().preset_bundle->is_bbl_vendor()) {
/*right*/ /*right*/
if (obj->GetExtderSystem()->GetTotalExtderCount() > 0) { if (obj->GetExtderSystem()->GetTotalExtderCount() > 0) {
auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID);
if (ext->HasFilamentInExt()) { if (ext->HasFilamentInExt()) {
if (ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) { if (ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3); m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3);
} else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
}
} else { } else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE); m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
} }
m_ams_control->SetExtruder(ext->HasFilamentInExt(), MAIN_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id); } else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
} }
m_ams_control->SetExtruder(ext->HasFilamentInExt(), MAIN_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id);
}
/*left*/ /*left*/
if (obj->GetExtderSystem()->GetTotalExtderCount() > 1) { if (obj->GetExtderSystem()->GetTotalExtderCount() > 1) {
auto ext = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID); auto ext = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID);
if (ext->HasFilamentInExt()) { if (ext->HasFilamentInExt()) {
if (ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) { if (ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3); m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3);
} else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
}
} else { } else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE); m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
} }
m_ams_control->SetExtruder(ext->HasFilamentInExt(), DEPUTY_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id); } else {
} m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
} else {
/*all extruders, in id order*/
for (const auto& ext : obj->GetExtderSystem()->GetExtruders()) {
if (ext.HasFilamentInExt()) {
if (ext.GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext.GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3);
} else {
m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
}
} else {
m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
}
m_ams_control->SetExtruder(ext.HasFilamentInExt(), ext.GetExtId(), ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id);
} }
m_ams_control->SetExtruder(ext->HasFilamentInExt(), DEPUTY_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id);
} }
update_filament_loading_panel(obj); update_filament_loading_panel(obj);
@@ -4306,48 +4185,33 @@ void StatusPanel::on_set_nozzle_temp(int nozzle_id)
try { try {
long nozzle_temp; long nozzle_temp;
if (wxGetApp().preset_bundle->is_bbl_vendor()) { if (nozzle_id == MAIN_EXTRUDER_ID) {
if (nozzle_id == MAIN_EXTRUDER_ID) { wxString str = m_tempCtrl_nozzle->GetTextCtrl()->GetValue();
wxString str = m_tempCtrl_nozzle->GetTextCtrl()->GetValue(); if (str.ToLong(&nozzle_temp) && obj) {
if (str.ToLong(&nozzle_temp) && obj) { set_hold_count(m_temp_nozzle_timeout);
set_hold_count(m_temp_nozzle_timeout); if (nozzle_temp > m_tempCtrl_nozzle->get_max_temp()) {
if (nozzle_temp > m_tempCtrl_nozzle->get_max_temp()) { nozzle_temp = m_tempCtrl_nozzle->get_max_temp();
nozzle_temp = m_tempCtrl_nozzle->get_max_temp(); m_tempCtrl_nozzle->SetTagTemp(wxString::Format("%d", nozzle_temp));
m_tempCtrl_nozzle->SetTagTemp(wxString::Format("%d", nozzle_temp)); m_tempCtrl_nozzle->Warning(false);
m_tempCtrl_nozzle->Warning(false);
}
if (m_tempCtrl_nozzle->GetCurrType() == TempInputType::TEMP_OF_NORMAL_TYPE) {
obj->command_set_nozzle(nozzle_temp);
} else {
obj->command_set_nozzle_new(MAIN_EXTRUDER_ID, nozzle_temp);
}
} }
} if (m_tempCtrl_nozzle->GetCurrType() == TempInputType::TEMP_OF_NORMAL_TYPE) {
obj->command_set_nozzle(nozzle_temp);
if (nozzle_id == DEPUTY_EXTRUDER_ID) { } else {
wxString str = m_tempCtrl_nozzle_deputy->GetTextCtrl()->GetValue(); obj->command_set_nozzle_new(MAIN_EXTRUDER_ID, nozzle_temp);
if (str.ToLong(&nozzle_temp) && obj) {
set_hold_count(m_temp_nozzle_deputy_timeout);
if (nozzle_temp > m_tempCtrl_nozzle_deputy->get_max_temp()) {
nozzle_temp = m_tempCtrl_nozzle_deputy->get_max_temp();
m_tempCtrl_nozzle_deputy->SetTagTemp(wxString::Format("%d", nozzle_temp));
m_tempCtrl_nozzle_deputy->Warning(false);
}
obj->command_set_nozzle_new(DEPUTY_EXTRUDER_ID, nozzle_temp);
} }
} }
} }
else if (nozzle_id >= 0 && nozzle_id < (int)m_tempCtrl_nozzles.size()) {
assert(m_tempCtrl_nozzles.size() == m_temp_nozzle_timeouts.size()); if (nozzle_id == DEPUTY_EXTRUDER_ID) {
wxString str = m_tempCtrl_nozzles[nozzle_id]->GetTextCtrl()->GetValue(); wxString str = m_tempCtrl_nozzle_deputy->GetTextCtrl()->GetValue();
if (str.ToLong(&nozzle_temp) && obj) { if (str.ToLong(&nozzle_temp) && obj) {
set_hold_count(m_temp_nozzle_timeouts[nozzle_id]); set_hold_count(m_temp_nozzle_deputy_timeout);
if (nozzle_temp > m_tempCtrl_nozzles[nozzle_id]->get_max_temp()) { if (nozzle_temp > m_tempCtrl_nozzle_deputy->get_max_temp()) {
nozzle_temp = m_tempCtrl_nozzles[nozzle_id]->get_max_temp(); nozzle_temp = m_tempCtrl_nozzle_deputy->get_max_temp();
m_tempCtrl_nozzles[nozzle_id]->SetTagTemp(wxString::Format("%d", nozzle_temp)); m_tempCtrl_nozzle_deputy->SetTagTemp(wxString::Format("%d", nozzle_temp));
m_tempCtrl_nozzles[nozzle_id]->Warning(false); m_tempCtrl_nozzle_deputy->Warning(false);
} }
obj->command_set_nozzle_new(nozzle_id, nozzle_temp); obj->command_set_nozzle_new(DEPUTY_EXTRUDER_ID, nozzle_temp);
} }
} }
} catch (...) { } catch (...) {
@@ -4431,44 +4295,26 @@ void StatusPanel::on_ams_load_curr()
return; return;
} }
// Filament Track Switch is calibrated: the load can go to either/any extruder, so ask // Filament Track Switch is calibrated: the load can go to either extruder, so ask the
// the user which. Record each extruder's currently-loaded slot so the dialog can grey // user which. Record each extruder's currently-loaded slot so the dialog can grey out a
// out a side that already holds this filament. // side that already holds this filament.
// NOTE: FeedDirectionDialog/SetExtruderMapping (Widgets/AMSItem.{hpp,cpp}) take the real std::vector<std::pair<std::string, std::string>> extruderSlots(2, {"", ""});
// extruder count in the non-BBL path now, but that class's own internals haven't been if (auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); ext.has_value())
// audited for N > 2 -- this only fixes what's local to StatusPanel.cpp. {
int extruder_count; if (ext->HasFilamentInExt())
std::vector<std::pair<std::string, std::string>> extruderSlots;
if (wxGetApp().preset_bundle->is_bbl_vendor()) {
extruder_count = 2;
extruderSlots.assign(2, {"", ""});
if (auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); ext.has_value())
{ {
if (ext->HasFilamentInExt()) extruderSlots[MAIN_EXTRUDER_ID] = {ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id};
{
extruderSlots[MAIN_EXTRUDER_ID] = {ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id};
}
} }
if (auto ext = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID); ext.has_value()) }
if (auto ext = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID); ext.has_value())
{
if (ext->HasFilamentInExt())
{ {
if (ext->HasFilamentInExt()) extruderSlots[DEPUTY_EXTRUDER_ID] = {ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id};
{
extruderSlots[DEPUTY_EXTRUDER_ID] = {ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id};
}
}
} else {
extruder_count = obj->GetExtderSystem()->GetTotalExtderCount();
extruderSlots.assign(extruder_count, {"", ""});
for (const auto& ext : obj->GetExtderSystem()->GetExtruders())
{
if (ext.HasFilamentInExt())
{
extruderSlots[ext.GetExtId()] = {ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id};
}
} }
} }
FeedDirectionDialog dialog(nullptr, extruder_count, obj->printer_type); FeedDirectionDialog dialog(nullptr, 2, obj->printer_type);
dialog.SetExtruderMapping(obj, curr_ams_id, curr_can_id, extruderSlots); dialog.SetExtruderMapping(obj, curr_ams_id, curr_can_id, extruderSlots);
auto rtn = dialog.ShowModal(); auto rtn = dialog.ShowModal();
@@ -4570,50 +4416,34 @@ void StatusPanel::on_ams_switch(SimpleEvent &event)
{ {
if(obj){ if(obj){
if (wxGetApp().preset_bundle->is_bbl_vendor()) { /*right*/
/*right*/ if (obj->GetExtderSystem()->GetTotalExtderCount() > 0) {
if (obj->GetExtderSystem()->GetTotalExtderCount() > 0) { auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID);
auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); if (ext->HasFilamentInExt()) {
if (ext->HasFilamentInExt()) { if (ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
if (ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) { m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3);
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3);
} else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
}
} else { } else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE); m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
} }
m_ams_control->SetExtruder(ext->HasFilamentInExt(), MAIN_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id); } else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
} }
m_ams_control->SetExtruder(ext->HasFilamentInExt(), MAIN_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id);
}
/*left*/ /*left*/
if (obj->GetExtderSystem()->GetTotalExtderCount() > 1) { if (obj->GetExtderSystem()->GetTotalExtderCount() > 1) {
auto ext = obj->GetExtderSystem()->GetExtderById(DEPUTY_EXTRUDER_ID); auto ext = obj->GetExtderSystem()->GetExtruders()[DEPUTY_EXTRUDER_ID];
if (ext->HasFilamentInExt()) { if (ext.HasFilamentInExt()) {
if (ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext->GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) { if (ext.GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext.GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3); m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3);
} else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
}
} else { } else {
m_ams_control->SetAmsStep(ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE); m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
} }
m_ams_control->SetExtruder(ext->HasFilamentInExt(), DEPUTY_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id); } else {
} m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
} else {
/*all extruders, in id order*/
for (const auto& ext : obj->GetExtderSystem()->GetExtruders()) {
if (ext.HasFilamentInExt()) {
if (ext.GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_MAIN_ID) || ext.GetSlotNow().ams_id == std::to_string(VIRTUAL_TRAY_DEPUTY_ID)) {
m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, "0", AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP3);
} else {
m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_LOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_COMBO_LOAD_STEP2);
}
} else {
m_ams_control->SetAmsStep(ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id, AMSPassRoadType::AMS_ROAD_TYPE_UNLOAD, AMSPassRoadSTEP::AMS_ROAD_STEP_NONE);
}
m_ams_control->SetExtruder(ext.HasFilamentInExt(), ext.GetExtId(), ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id);
} }
m_ams_control->SetExtruder(ext.HasFilamentInExt(), DEPUTY_EXTRUDER_ID, ext.GetSlotNow().ams_id, ext.GetSlotNow().slot_id);
} }
} }
} }
@@ -5539,17 +5369,10 @@ void StatusPanel::msw_rescale()
m_bpButton_xy->Rescale(); m_bpButton_xy->Rescale();
auto size = TEMP_CTRL_MIN_SIZE_ALIGN_ONE_ICON; auto size = TEMP_CTRL_MIN_SIZE_ALIGN_ONE_ICON;
if (obj && obj->GetExtderSystem()->GetTotalExtderCount() >= 2) size = TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON; if (obj && obj->GetExtderSystem()->GetTotalExtderCount() >= 2) size = TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON;
if (wxGetApp().preset_bundle->is_bbl_vendor()) { m_tempCtrl_nozzle->SetMinSize(size);
m_tempCtrl_nozzle->SetMinSize(size); m_tempCtrl_nozzle->Rescale();
m_tempCtrl_nozzle->Rescale(); m_tempCtrl_nozzle_deputy->SetMinSize(size);
m_tempCtrl_nozzle_deputy->SetMinSize(size); m_tempCtrl_nozzle_deputy->Rescale();
m_tempCtrl_nozzle_deputy->Rescale();
} else {
for (auto& tempCtrl : m_tempCtrl_nozzles) {
tempCtrl->SetMinSize(size);
tempCtrl->Rescale();
}
}
m_line_nozzle->SetSize(wxSize(-1, FromDIP(1))); m_line_nozzle->SetSize(wxSize(-1, FromDIP(1)));
m_tempCtrl_bed->SetMinSize(size); m_tempCtrl_bed->SetMinSize(size);
m_tempCtrl_bed->Rescale(); m_tempCtrl_bed->Rescale();
-4
View File
@@ -482,10 +482,6 @@ protected:
int m_temp_nozzle_timeout{ 0 }; int m_temp_nozzle_timeout{ 0 };
TempInput* m_tempCtrl_nozzle_deputy; TempInput* m_tempCtrl_nozzle_deputy;
int m_temp_nozzle_deputy_timeout{ 0 }; int m_temp_nozzle_deputy_timeout{ 0 };
std::vector<TempInput*> m_tempCtrl_nozzles;
std::vector<int> m_temp_nozzle_timeouts;
TempInput * m_tempCtrl_bed; TempInput * m_tempCtrl_bed;
int m_temp_bed_timeout {0}; int m_temp_bed_timeout {0};
TempInput * m_tempCtrl_chamber; TempInput * m_tempCtrl_chamber;
+1 -1
View File
@@ -700,7 +700,7 @@ SyncAmsInfoDialog::SyncAmsInfoDialog(wxWindow *parent, SyncInfo &info) :
//wxBoxSizer *m_scroll_sizer = new wxBoxSizer(wxVERTICAL); //wxBoxSizer *m_scroll_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(m_show_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(m_show_page, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetBackgroundColour(*wxWHITE); m_scrolledWindow->SetBackgroundColour(*wxWHITE);
m_scrolledWindow->SetScrollRate(0, 20); m_scrolledWindow->SetScrollRate(0, FromDIP(20));
m_scrolledWindow->SetMinSize(wxSize(-1, SyncAmsInfoDialogHeightMAX)); m_scrolledWindow->SetMinSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
m_scrolledWindow->SetMaxSize(wxSize(-1, SyncAmsInfoDialogHeightMAX)); m_scrolledWindow->SetMaxSize(wxSize(-1, SyncAmsInfoDialogHeightMAX));
m_scrolledWindow->EnableScrolling(false,true); m_scrolledWindow->EnableScrolling(false,true);
+13 -2
View File
@@ -3069,6 +3069,7 @@ void TabPrint::build()
optgroup->append_single_option_line("wipe_tower_rib_width", "multimaterial_settings_prime_tower#rib-width"); optgroup->append_single_option_line("wipe_tower_rib_width", "multimaterial_settings_prime_tower#rib-width");
optgroup->append_single_option_line("wipe_tower_fillet_wall", "multimaterial_settings_prime_tower#fillet-wall"); optgroup->append_single_option_line("wipe_tower_fillet_wall", "multimaterial_settings_prime_tower#fillet-wall");
optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers"); optgroup->append_single_option_line("wipe_tower_no_sparse_layers", "multimaterial_settings_prime_tower#no-sparse-layers");
optgroup->append_single_option_line("wipe_tower_sparse_layers_combination", "multimaterial_settings_prime_tower#combine-sparse-layers");
optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower"); optgroup->append_single_option_line("single_extruder_multi_material_priming", "multimaterial_settings_prime_tower");
optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features"); optgroup = page->new_optgroup(L("Filament for Features"), L"param_filament_for_features");
@@ -7165,12 +7166,18 @@ void Tab::restore_last_select_item()
bool Tab::page_build_pending() const bool Tab::page_build_pending() const
{ {
return m_active_page != nullptr && m_active_page->build_pending(); return m_active_page != nullptr && (m_active_page->build_pending() || m_active_page->visibility_pending());
} }
bool Tab::page_build_step() bool Tab::page_build_step()
{ {
return m_active_page != nullptr && m_active_page->build_step(m_mode); if (m_active_page == nullptr)
return false;
if (m_active_page->build_pending())
m_active_page->build_step(m_mode);
else
m_active_page->update_visibility(m_mode, true);
return page_build_pending();
} }
void Tab::update_description_lines() void Tab::update_description_lines()
@@ -8601,6 +8608,8 @@ void Page::update_visibility(ConfigOptionMode mode, bool update_contolls_visibil
} }
m_show = ret_val; m_show = ret_val;
if (update_contolls_visibility)
m_visibility_applied = true;
#ifdef __WXMSW__ #ifdef __WXMSW__
if (!m_show) return; if (!m_show) return;
// BBS: fix field control position // BBS: fix field control position
@@ -8654,6 +8663,7 @@ bool Page::activate_group(size_t i, ConfigOptionMode mode, std::function<void()>
auto& group = m_optgroups[i]; auto& group = m_optgroups[i];
if (!group->activate(throw_if_canceled)) if (!group->activate(throw_if_canceled))
return false; return false;
m_visibility_applied = false;
m_vsizer->Add(group->sizer, 0, wxEXPAND | (group->is_legend_line() ? (wxLEFT|wxTOP) : wxALL), m_parent->FromDIP(5)); // ORCA use less margin on parameters section m_vsizer->Add(group->sizer, 0, wxEXPAND | (group->is_legend_line() ? (wxLEFT|wxTOP) : wxALL), m_parent->FromDIP(5)); // ORCA use less margin on parameters section
group->update_visibility(mode); group->update_visibility(mode);
#if HIDE_FIRST_SPLIT_LINE #if HIDE_FIRST_SPLIT_LINE
@@ -8688,6 +8698,7 @@ void Page::clear()
for (auto group : m_optgroups) for (auto group : m_optgroups)
group->clear(); group->clear();
m_page_title = NULL; m_page_title = NULL;
m_visibility_applied = false;
} }
void Page::msw_rescale() void Page::msw_rescale()
+5 -2
View File
@@ -71,6 +71,7 @@ class Page: public std::enable_shared_from_this<Page>// : public wxScrolledWindo
// BBS: new layout // BBS: new layout
wxStaticText* m_page_title; wxStaticText* m_page_title;
bool m_show = true; bool m_show = true;
bool m_visibility_applied = false;
public: public:
//BBS: GUI refactor //BBS: GUI refactor
Page(wxWindow* parent, const wxString& title, int iconID, wxPanel* tab_owner); Page(wxWindow* parent, const wxString& title, int iconID, wxPanel* tab_owner);
@@ -98,6 +99,8 @@ public:
bool build_pending() const; bool build_pending() const;
// Builds the next option group that has no controls yet; true while some remain. // Builds the next option group that has no controls yet; true while some remain.
bool build_step(ConfigOptionMode mode); bool build_step(ConfigOptionMode mode);
// Whether the controls have not been shown or hidden for a mode since they were built.
bool visibility_pending() const { return !m_visibility_applied; }
void clear(); void clear();
void msw_rescale(); void msw_rescale();
void sys_color_changed(); void sys_color_changed();
@@ -443,8 +446,8 @@ public:
// BBS: new layout // BBS: new layout
void set_expanded(bool value); void set_expanded(bool value);
void restore_last_select_item(); void restore_last_select_item();
// page_build_pending() says whether the selected page has groups without controls, and // page_build_pending() says whether the selected page has groups without controls or controls
// page_build_step() builds one. // not yet shown for the mode, and page_build_step() does the next of those.
bool page_build_pending() const; bool page_build_pending() const;
bool page_build_step(); bool page_build_step();
+3 -3
View File
@@ -534,7 +534,7 @@ public:
m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_content = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_content->SetBackgroundColour(pop_bg); m_content->SetBackgroundColour(pop_bg);
m_content->SetScrollRate(0, FromDIP(5)); m_content->SetScrollRate(0, FromDIP(20));
auto* outer = new wxBoxSizer(wxVERTICAL); auto* outer = new wxBoxSizer(wxVERTICAL);
const int pop_w = std::max(FromDIP(213), popup_width); const int pop_w = std::max(FromDIP(213), popup_width);
@@ -2274,7 +2274,7 @@ void TextureImportDialog::build_mapping_panel(wxWindow* parent, wxSizer* sizer)
m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition, m_mapping_scroll = new wxScrolledWindow(parent, wxID_ANY, wxDefaultPosition,
wxSize(-1, FromDIP(300))); wxSize(-1, FromDIP(300)));
m_mapping_scroll->SetScrollRate(0, FromDIP(10)); m_mapping_scroll->SetScrollRate(0, FromDIP(20));
m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE)); m_mapping_scroll->SetBackgroundColour(StateColor::darkModeColorFor(*wxWHITE));
m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this); m_mapping_scroll->Bind(wxEVT_MOUSEWHEEL, &TextureImportDialog::dismiss_filament_popup_on_wheel, this);
@@ -4335,7 +4335,7 @@ void TextureImportDialog::on_dpi_changed(const wxRect&)
if (m_mapping_scroll) { if (m_mapping_scroll) {
m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300))); m_mapping_scroll->SetMinSize(wxSize(-1, FromDIP(300)));
m_mapping_scroll->SetScrollRate(0, FromDIP(10)); m_mapping_scroll->SetScrollRate(0, FromDIP(20));
} }
if (m_btn_skip) { if (m_btn_skip) {
+1 -1
View File
@@ -942,7 +942,7 @@ void UnsavedChangesDialog::build(Preset::Type type, PresetCollection *dependent_
m_sizer_tab->Add(m_table_top, 1, 0, 0); m_sizer_tab->Add(m_table_top, 1, 0, 0);
m_scrolledWindow = new wxScrolledWindow(m_panel_tab, wxID_ANY, wxDefaultPosition, UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE, wxNO_BORDER|wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(m_panel_tab, wxID_ANY, wxDefaultPosition, UNSAVE_CHANGE_DIALOG_SCROLL_WINDOW_SIZE, wxNO_BORDER|wxVSCROLL);
m_scrolledWindow->SetScrollRate(0, 5); m_scrolledWindow->SetScrollRate(0, FromDIP(20));
m_scrolledWindow->SetBackgroundColour(GREY200); m_scrolledWindow->SetBackgroundColour(GREY200);
m_sizer_bottom = new wxBoxSizer(wxVERTICAL); m_sizer_bottom = new wxBoxSizer(wxVERTICAL);
m_sizer_bottom->Add(m_scrolledWindow, 1, wxEXPAND, 0); m_sizer_bottom->Add(m_scrolledWindow, 1, wxEXPAND, 0);
+1 -1
View File
@@ -130,7 +130,7 @@ MsgUpdateConfig::MsgUpdateConfig(const std::vector<Update> &updates, bool force_
m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15)); m_sizer_right->Add(0, 0, 1, wxTOP, FromDIP(15));
auto m_scrollwindw_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)),wxVSCROLL); auto m_scrollwindw_release_note = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxSize(FromDIP(560), FromDIP(430)),wxVSCROLL);
m_scrollwindw_release_note->SetScrollRate(0, 5); m_scrollwindw_release_note->SetScrollRate(0, FromDIP(20));
m_scrollwindw_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8)); m_scrollwindw_release_note->SetBackgroundColour(wxColour(0xF8, 0xF8, 0xF8));
m_scrollwindw_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430))); m_scrollwindw_release_note->SetMaxSize(wxSize(FromDIP(560), FromDIP(430)));
m_scrollwindw_release_note->SetWindowStyle(wxVSCROLL); m_scrollwindw_release_note->SetWindowStyle(wxVSCROLL);
+1 -1
View File
@@ -1629,7 +1629,7 @@ UpgradePanel::UpgradePanel(wxWindow *parent, wxWindowID id, const wxPoint &pos,
auto m_main_sizer = new wxBoxSizer(wxVERTICAL); auto m_main_sizer = new wxBoxSizer(wxVERTICAL);
m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL); m_scrolledWindow = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL);
m_scrolledWindow->SetScrollRate(5, 25); m_scrolledWindow->SetScrollRate(5, FromDIP(20));
m_machine_list_sizer = new wxBoxSizer(wxVERTICAL); m_machine_list_sizer = new wxBoxSizer(wxVERTICAL);
+2 -2
View File
@@ -62,7 +62,7 @@ CheckList::CheckList(
s_sizer = new wxBoxSizer(wxVERTICAL); s_sizer = new wxBoxSizer(wxVERTICAL);
m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, scroll_style); m_scroll_area = new wxScrolledWindow(this, wxID_ANY, wxDefaultPosition, wxDefaultSize, scroll_style);
m_scroll_area->SetScrollRate(0, 10); m_scroll_area->SetScrollRate(0, FromDIP(20));
m_scroll_area->SetSizer(s_sizer); m_scroll_area->SetSizer(s_sizer);
m_scroll_area->SetBackgroundColour(parent->GetBackgroundColour()); m_scroll_area->SetBackgroundColour(parent->GetBackgroundColour());
m_scroll_area->Bind(wxEVT_RIGHT_DOWN, &CheckList::ShowMenu, this); m_scroll_area->Bind(wxEVT_RIGHT_DOWN, &CheckList::ShowMenu, this);
@@ -223,4 +223,4 @@ void CheckList::ShowMenu(wxMouseEvent &evt)
wxPoint screen_pos = src->ClientToScreen(evt.GetPosition()); wxPoint screen_pos = src->ClientToScreen(evt.GetPosition());
wxPoint local_pos = ScreenToClient(screen_pos); wxPoint local_pos = ScreenToClient(screen_pos);
PopupMenu(&m, local_pos); PopupMenu(&m, local_pos);
} }
+3 -10
View File
@@ -837,21 +837,14 @@ void MultiNozzleStatusTable::UpdateRackInfo(std::weak_ptr<DevNozzleRack> rack)
bool has_right = false; bool has_right = false;
for (auto& elem : nozzles_in_extruder) { for (auto& elem : nozzles_in_extruder) {
auto& nozzle = elem.second; auto& nozzle = elem.second;
int extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
int extruder_id{}; if (nozzle.AtRightExtruder())
if (wxGetApp().preset_bundle->is_bbl_vendor()) { has_right = true;
extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
if (nozzle.AtRightExtruder())
has_right = true;
}
else
extruder_id = nozzle.GetExtruderId();
NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow); NozzleVolumeType volume_type = DevNozzle::ToNozzleVolumeType(nozzle.m_nozzle_flow);
m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type); m_badge->SetExtruderInfo(extruder_id, format_diameter_to_str(nozzle.GetNozzleDiameter()), volume_type);
} }
// TODO: Update for N extruders
m_badge->SetExtruderValid(has_right); m_badge->SetExtruderValid(has_right);
} }
} }
+1 -1
View File
@@ -199,7 +199,7 @@ bool ProgressDialog::Create(const wxString &title, const wxString &message, int
m_sizer_main->Add(m_simplebook, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28)); m_sizer_main->Add(m_simplebook, 1, wxEXPAND | wxLEFT | wxRIGHT, FromDIP(28));
} else { } else {
m_msg_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL ); m_msg_scrolledWindow = new wxScrolledWindow( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxVSCROLL );
m_msg_scrolledWindow->SetScrollRate(0,5); m_msg_scrolledWindow->SetScrollRate(0, FromDIP(20));
wxBoxSizer* m_msg_sizer= new wxBoxSizer(wxVERTICAL); wxBoxSizer* m_msg_sizer= new wxBoxSizer(wxVERTICAL);
m_msg = new wxStaticText(m_msg_scrolledWindow, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1), 0); m_msg = new wxStaticText(m_msg_scrolledWindow, wxID_ANY, wxEmptyString, wxDefaultPosition, wxSize(PROGRESSDIALOG_SIMPLEBOOK_SIZE.x, -1), 0);
-3
View File
@@ -170,9 +170,6 @@ void TempInput::SetFinish()
wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH); wxCommandEvent event(wxCUSTOMEVT_SET_TEMP_FINISH);
event.SetInt(temp_type); event.SetInt(temp_type);
event.SetString(wxString::Format("%d", m_input_type)); event.SetString(wxString::Format("%d", m_input_type));
// N-extruder temp controls all share TEMP_OF_NORMAL_TYPE, so the string payload above can't
// tell them apart; carry widget identity so the listener can find which one fired.
event.SetEventObject(this);
wxPostEvent(this->GetParent(), event); wxPostEvent(this->GetParent(), event);
} }
+7 -3
View File
@@ -10,6 +10,7 @@
#include <wx/webviewarchivehandler.h> #include <wx/webviewarchivehandler.h>
#include <wx/webviewfshandler.h> #include <wx/webviewfshandler.h>
#include <wx/weakref.h>
#if wxUSE_WEBVIEW_EDGE #if wxUSE_WEBVIEW_EDGE
#include <wx/msw/webview_edge.h> #include <wx/msw/webview_edge.h>
#elif defined(__WXMAC__) #elif defined(__WXMAC__)
@@ -235,7 +236,9 @@ class FakeWebView : public wxWebView
wxDEFINE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent); wxDEFINE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
static std::vector<wxWebView*> g_webviews; static std::vector<wxWebView*> g_webviews;
static std::vector<wxWebView*> g_delay_webviews; // Webviews waiting for their script handler while another one is added; adding it yields, so a
// view can be destroyed while it waits.
static std::vector<wxWeakRef<wxWebView>> g_delay_webviews;
class WebViewRef : public wxObjectRefData class WebViewRef : public wxObjectRefData
{ {
@@ -340,8 +343,9 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
addScriptMessageHandler(webView); addScriptMessageHandler(webView);
while (!g_delay_webviews.empty()) { while (!g_delay_webviews.empty()) {
auto views = std::move(g_delay_webviews); auto views = std::move(g_delay_webviews);
for (auto wv : views) for (const wxWeakRef<wxWebView>& wv : views)
addScriptMessageHandler(wv); if (wv)
addScriptMessageHandler(wv.get());
} }
} }
#ifndef __WIN32__ #ifndef __WIN32__
+1 -1
View File
@@ -1222,7 +1222,7 @@ ImageTransientPopup::ImageTransientPopup( wxWindow *parent, bool scrolled, wxBit
m_panel->SetSize(300, 300); m_panel->SetSize(300, 300);
// And also actually enable them. // And also actually enable them.
m_panel->SetScrollRate(10, 10); m_panel->SetScrollRate(10, FromDIP(20));
} }
else else
{ {
+228
View File
@@ -2,6 +2,7 @@
#include <algorithm> #include <algorithm>
#include <cmath> #include <cmath>
#include <functional>
#include <map> #include <map>
#include <numeric> #include <numeric>
#include <sstream> #include <sstream>
@@ -11,8 +12,10 @@
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/AABBTreeLines.hpp" #include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Flow.hpp" #include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp"
#include "libslic3r/Layer.hpp" #include "libslic3r/Layer.hpp"
#include "libslic3r/Print.hpp" #include "libslic3r/Print.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
@@ -1196,6 +1199,231 @@ TEST_CASE("Trapezoidal grid infill rounds its corners only with more than one li
REQUIRE(single_smooth.length == single_sharp.length); REQUIRE(single_smooth.length == single_sharp.length);
} }
TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
{
const int multiline = GENERATE(2, 3);
const double spacing = 0.45;
const double density = 0.3;
const double wall = multiline * spacing;
CAPTURE(multiline);
const ExPolygon region{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(40., 0.),
Point::new_scale(40., 40.), Point::new_scale(0., 40.) } };
auto fill = [&region, spacing](int lines, double density, size_t layer_id, double z) {
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("cubic"));
filler->spacing = spacing;
filler->angle = float(M_PI / 7.);
filler->layer_id = layer_id;
filler->z = z;
FillParams params;
params.density = float(density);
params.multiline = lines;
params.dont_adjust = true;
params.anchor_length_max = 0.f; // The bare pattern, without connections along the boundary.
Slic3r::Surface surface(stInternal, region);
return filler->fill_surface(&surface, params);
};
// Away from the boundary, where a line is clipped earlier than the side of its wall.
const Polygons inner = shrink(to_polygons(region), scale_(3.));
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
double distance = 0.;
for (const Polyline &path : intersection_pl(from, inner))
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
distance = std::max(distance, tree.distance_from_lines<false>(point));
return unscale<double>(distance);
};
// One z period of the pattern: sqrt(2) / 3 of the 3 * wall / density line spacing.
const double z_period = std::sqrt(2.) * wall / density;
const size_t layers = 30;
for (size_t layer_id = 0; layer_id < layers; ++layer_id) {
const double z = z_period * (layer_id + 0.5) / layers;
CAPTURE(layer_id, z);
const Polylines walls = fill(multiline, density, layer_id, z);
REQUIRE_FALSE(walls.empty());
CHECK(get_intersections(to_lines(walls)).empty());
// Long paths running out to the boundary, not loops around the cells.
CHECK(std::none_of(walls.begin(), walls.end(), [](const Polyline &path) { return path.first_point() == path.last_point(); }));
// Single lines at the same spacing: the walls are drawn along them.
const Polylines lines = fill(1, density / multiline, layer_id, z);
REQUIRE_FALSE(lines.empty());
CHECK(farthest(lines, walls) < 0.5 * wall);
CHECK(farthest(walls, lines) < 1.5 * wall);
}
}
TEST_CASE("Multiline adaptive cubic infill keeps its lines apart without closing them around the cells", "[Fill]")
{
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
const int multiline = GENERATE(2, 3);
CAPTURE(pattern, multiline);
// A sphere refines the octree all around, so the finer lines end on the coarser ones at every layer.
TriangleMesh sphere = Slic3r::Test::mesh(Slic3r::Test::TestMesh::sphere_50mm);
sphere.scale(0.3f);
Print print;
Slic3r::Test::init_and_process_print({sphere}, print,
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "40%"},
{"fill_multiline", multiline},
{"infill_anchor", 0},
{"infill_anchor_max", 0},
{"layer_height", 0.3}});
size_t paths = 0, loops = 0;
for (const Layer *layer : print.objects().front()->layers()) {
Polylines printed;
Polygons sparse;
double spacing = 0.;
for (const LayerRegion *region : layer->regions()) {
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
if (entity->role() == erInternalInfill)
entity->collect_polylines(printed);
for (const Surface &surface : region->fill_surfaces.surfaces)
if (surface.surface_type == stInternal)
append(sparse, shrink(to_polygons(surface.expolygon), scale_(1.)));
spacing = region->flow(frInfill).spacing();
}
if (printed.empty())
continue;
CAPTURE(layer->print_z);
paths += printed.size();
loops += std::count_if(printed.begin(), printed.end(), [](const Polyline &pl) { return pl.first_point() == pl.last_point(); });
CHECK(get_intersections(to_lines(printed)).empty());
// Neighbouring lines stay a line spacing apart, less the overlap of a line end with the wall it stops on.
// Pieces of one line that meet end to end are one line.
std::vector<size_t> line_of(printed.size());
std::iota(line_of.begin(), line_of.end(), 0);
std::function<size_t(size_t)> find = [&](size_t i) { return line_of[i] == i ? i : line_of[i] = find(line_of[i]); };
for (size_t i = 0; i < printed.size(); ++i)
for (size_t j = i + 1; j < printed.size(); ++j)
for (const Point &a : { printed[i].first_point(), printed[i].last_point() })
for (const Point &b : { printed[j].first_point(), printed[j].last_point() })
if ((a - b).cast<double>().norm() < SCALED_EPSILON)
line_of[find(i)] = find(j);
Lines lines;
std::vector<size_t> owner;
for (size_t i = 0; i < printed.size(); ++i)
for (const Line &line : printed[i].lines()) {
lines.push_back(line);
owner.push_back(find(i));
}
AABBTreeLines::LinesDistancer<Line> tree(lines);
double closest = spacing;
for (size_t i = 0; i < printed.size(); ++i)
for (const Point &p : printed[i].equally_spaced_points(scale_(0.1)))
if (contains(sparse, p))
for (size_t k : tree.all_lines_in_radius(p, scale_(spacing)))
if (owner[k] != find(i))
closest = std::min(closest, unscale<double>(lines[k].distance_to(p)));
CHECK(closest > 0.45 * spacing);
}
REQUIRE(paths > 0);
// The lines run on through the cells instead of each cell getting its own loops.
CHECK(loops < paths / 4);
}
TEST_CASE("Multiline adaptive cubic paths touch where they bounce off each other", "[Fill]")
{
const int sweep = GENERATE(0, 1, 2);
// Offset of the third family in walls, so the three meet in points or in small triangles.
const double shift = GENERATE(0., 0.1, 0.5, 1., 2.5, -0.5, -1.);
// Like finer octree lines ending on coarser ones, the 60 degree lines may start on the horizontal line through 0.
const bool starting = GENERATE(false, true);
CAPTURE(sweep, shift, starting);
const double d1 = scale_(0.8), pitch = scale_(8.), inner = scale_(12.);
Lines lines;
for (int k = 0; k < 3; ++k) {
const Vec2d dir(std::cos(k * M_PI / 3.), std::sin(k * M_PI / 3.)), normal(-dir.y(), dir.x());
for (int i = -6; i <= 6; ++i) {
const Vec2d mid = (i * pitch + (k == 2 ? shift * d1 : 0.)) * normal;
const double start = k == 1 && starting ? -mid.y() / dir.y() : -10. * pitch;
lines.emplace_back((mid + start * dir).cast<coord_t>(), (mid + 10. * pitch * dir).cast<coord_t>());
}
}
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, 0., sweep, BoundingBox(Point::new_scale(-20., -20.), Point::new_scale(20., 20.)));
REQUIRE_FALSE(paths.empty());
CHECK(get_intersections(to_lines(paths)).empty());
Lines pieces;
std::vector<size_t> owner;
for (size_t i = 0; i < paths.size(); ++i)
for (const Line &line : paths[i].lines()) {
pieces.push_back(line);
owner.push_back(i);
}
AABBTreeLines::LinesDistancer<Line> tree(pieces);
auto clearance = [&](size_t i) {
const Line &a = pieces[i];
double distance = std::numeric_limits<double>::max();
for (size_t j : tree.all_lines_in_radius(a.midpoint(), 0.5 * a.length() + 2. * d1))
if (owner[j] != owner[i]) {
const Line &b = pieces[j];
distance = std::min({ distance, a.distance_to(b.a), a.distance_to(b.b), b.distance_to(a.a), b.distance_to(a.b) });
}
return distance;
};
auto inside = [inner](const Point &p) { return std::abs(p.x()) < inner && std::abs(p.y()) < inner; };
double closest = std::numeric_limits<double>::max();
for (size_t i = 0; i < pieces.size(); ++i)
if (inside(pieces[i].midpoint()))
closest = std::min(closest, clearance(i));
CHECK(closest > 0.99 * d1);
// Each path at a crossing touches another one there, none stops short of it.
double widest = 0.;
for (size_t i = 0; i < lines.size(); ++i)
for (size_t j = i + 1; j < lines.size(); ++j)
if (Point crossing; line_alg::intersection(lines[i], lines[j], &crossing) && inside(crossing)) {
std::map<size_t, double> at;
for (size_t k : tree.all_lines_in_radius(crossing, 1.2 * d1))
at.emplace(owner[k], std::numeric_limits<double>::max());
for (size_t k : tree.all_lines_in_radius(crossing, 2. * d1))
if (auto it = at.find(owner[k]); it != at.end())
it->second = std::min(it->second, clearance(k));
for (const auto &path : at)
widest = std::max(widest, path.second);
}
CHECK(widest < 1.02 * d1);
}
TEST_CASE("Multiline adaptive cubic paths reach the line they end on when another path ends on them", "[Fill]")
{
const int sweep = GENERATE(0, 1, 2);
// Where the 120 degree line starts on the horizontal one, in walls from the 60 degree line.
const double start = GENERATE(0.3, 0.6, 1., 2.);
CAPTURE(sweep, start);
const double d1 = scale_(0.8), overlap = 0.1 * d1, length = scale_(30.);
const Vec2d diagonal(0.5, 0.5 * std::sqrt(3.)), horizontal(1., 0.), steep(-0.5, 0.5 * std::sqrt(3.));
const Vec2d on_horizontal = start * d1 * horizontal;
const Lines lines{ Line((-length * diagonal).cast<coord_t>(), (length * diagonal).cast<coord_t>()),
Line(Point(0, 0), (length * horizontal).cast<coord_t>()),
Line(on_horizontal.cast<coord_t>(), (on_horizontal - length * steep).cast<coord_t>()) };
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, overlap, sweep, BoundingBox(Point::new_scale(-40., -40.), Point::new_scale(40., 40.)));
// The end of the path along each line nearest to where that line starts.
auto end_along = [&paths](const Line &line) {
for (const Polyline &path : paths)
if (line.distance_to(path.first_point()) < SCALED_EPSILON && line.distance_to(path.last_point()) < SCALED_EPSILON)
return (path.first_point() - line.a).cast<double>().norm() < (path.last_point() - line.a).cast<double>().norm() ? path.first_point() : path.last_point();
return Point(std::numeric_limits<coord_t>::max(), 0);
};
const Point horizontal_end = end_along(lines[1]), steep_end = end_along(lines[2]);
REQUIRE(horizontal_end.x() != std::numeric_limits<coord_t>::max());
REQUIRE(steep_end.x() != std::numeric_limits<coord_t>::max());
// Both reach the overlap into the wall of the path they stop at, none stops short of it.
CHECK_THAT(line_alg::distance_to_infinite(lines[0], horizontal_end) / d1, Catch::Matchers::WithinAbs(0.9, 0.01));
CHECK(lines[1].distance_to(steep_end) / d1 < 0.91);
CHECK(get_intersections(to_lines(paths)).empty());
}
TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]") TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]")
{ {
auto shape_for = [](const std::string &smooth_factor) { auto shape_for = [](const std::string &smooth_factor) {
+94
View File
@@ -630,3 +630,97 @@ TEST_CASE("A lower layer sliver too thin to print does not support the wall abov
// A rib that does get printed takes the 20mm outer wall running along it out of the overhangs. // A rib that does get printed takes the 20mm outer wall running along it out of the overhangs.
CHECK(printable < no_rib - scale_(15.)); CHECK(printable < no_rib - scale_(15.));
} }
namespace {
// Every setting the fuzzy skin assertions below depend on.
DynamicPrintConfig fuzzy_skin_config(const char *wall_generator)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "wall_generator", wall_generator },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
// One wall, so every wall point along the long sides belongs to the fuzzed outer wall.
{ "wall_loops", 1 },
{ "fuzzy_skin", "external" },
{ "fuzzy_skin_noise_type", "classic" },
{ "fuzzy_skin_thickness", 0.3 },
{ "fuzzy_skin_point_distance", 0.8 },
});
return config;
}
// How far the wall points over the middle 60% of the layer's length stray across its width, worst side.
// A negative result means there is no layer at `print_z`.
double mid_span_wall_spread(const Print &print, double print_z)
{
for (const Layer *layer : print.objects().front()->layers()) {
if (std::abs(layer->print_z - print_z) > 1e-4)
continue;
const BoundingBox bbox = get_extents(layer->lslices);
const coord_t x_min = bbox.min.x() + bbox.size().x() / 5;
const coord_t x_max = bbox.max.x() - bbox.size().x() / 5;
Points points;
for (const LayerRegion *region : layer->regions())
region->perimeters.collect_points(points);
coord_t spread = 0;
for (const bool south : { true, false }) {
coord_t lo = bbox.max.y(), hi = bbox.min.y();
for (const Point &p : points)
if (p.x() > x_min && p.x() < x_max && (p.y() < bbox.center().y()) == south) {
lo = std::min(lo, p.y());
hi = std::max(hi, p.y());
}
spread = std::max(spread, hi - lo);
}
return unscale<double>(spread);
}
return -1.;
}
} // namespace
// TestMesh::bridge is a 50x10mm deck from z=5 to z=8 on two 5mm-wide pillars, leaving a 40mm span. The deck's
// first layer (print_z 5.2) crosses the span unsupported; the layers above it rest on the deck.
TEST_CASE("Fuzzy skin leaves the walls of a bridge smooth", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
Print print;
init_and_process_print({ TestMesh::bridge }, print, fuzzy_skin_config(wall_generator));
REQUIRE_FALSE(print.objects().empty());
// Control: one deck layer up the same walls rest on the deck, so they are fuzzed.
CHECK(mid_span_wall_spread(print, 5.6) > 0.1);
// Over the unsupported span the walls stay straight.
const double bridged = mid_span_wall_spread(print, 5.2);
CHECK(bridged >= 0.);
CHECK(bridged < 0.001);
}
// One object: a 20x20x3mm block on the bed and a second one floating above it from z=5 to z=8. The layers in
// the gap are empty, so the floating block's first layer (print_z 5.2) has a layer below it with nothing
// printed on it; the layers above rest on the floating block.
TEST_CASE("Fuzzy skin leaves the walls over an empty layer smooth", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
TriangleMesh mesh = make_cube(20., 20., 3.);
TriangleMesh floating = make_cube(20., 20., 3.);
floating.translate(0.f, 0.f, 5.f);
mesh.merge(floating);
Print print;
init_and_process_print({ mesh }, print, fuzzy_skin_config(wall_generator));
REQUIRE_FALSE(print.objects().empty());
// Control: one layer up the walls rest on the floating block, so they are fuzzed.
CHECK(mid_span_wall_spread(print, 5.6) > 0.1);
// Nothing is printed under the first floating layer, so its walls stay straight.
const double floating_first_layer = mid_span_wall_spread(print, 5.2);
CHECK(floating_first_layer >= 0.);
CHECK(floating_first_layer < 0.001);
}
+113
View File
@@ -308,6 +308,119 @@ TEST_CASE("A single-filament plate reserves a tower only when one is actually pr
} }
} }
// Filament 2 on the top surface only, so every layer below it is a toolchange-free tower layer: the
// run "Combine sparse layers" folds. The two heights decide whether anything folds, so they are the
// caller's business.
static DynamicPrintConfig sparse_run_config(double layer_height, const char *max_layer_height, bool combine)
{
DynamicPrintConfig config = multifilament_config(2, {
{ "top_surface_filament_id", 2 },
{ "enable_prime_tower", true },
{ "wipe_tower_x", 50 }, // inside the 200x200 test bed
{ "wipe_tower_y", 50 },
{ "prime_tower_width", 35 },
{ "min_layer_height", "0.08"},
{ "single_extruder_multi_material", true },
{ "timelapse_type", "0" },
{ "enable_wrapping_detection", false },
{ "raft_layers", "0" } });
// A taller first layer would top the plan and hide what the run does, so slice at one height.
config.set_deserialize_strict({ { "layer_height", std::to_string(layer_height) },
{ "initial_layer_print_height", std::to_string(layer_height) },
{ "max_layer_height", max_layer_height },
{ "wipe_tower_sparse_layers_combination", combine ? "1" : "0" } });
return config;
}
// What a sliced tower did with its sparse run.
struct SparseRunResult { size_t planned, sparse, folded; float tallest_printed, printed_height; std::string gcode; };
static SparseRunResult slice_sparse_run(const DynamicPrintConfig &config)
{
Print print;
Model model;
init_print({ cube(10) }, print, model, config);
print.apply(model, config);
print.process();
REQUIRE(print.is_step_done(psWipeTower));
SparseRunResult r{};
for (const std::vector<WipeTower::ToolChangeResult> &layer : print.wipe_tower_data().tool_changes) {
if (layer.empty())
continue;
++r.planned;
if (wipe_tower_layer_is_sparse(layer))
++r.sparse;
if (wipe_tower_layer_is_combined_away(layer)) {
++r.folded;
} else {
r.tallest_printed = std::max(r.tallest_printed, layer.front().layer_height);
r.printed_height += layer.front().layer_height;
}
}
r.gcode = Slic3r::Test::gcode(print);
return r;
}
// How often the G-code declares `height` in the tag this printer's processor reads. The dialect is a
// global the exporter sets from the printer, so this is only correct after a slice - the point below.
static size_t count_height_tags(const std::string &gcode, const char *height)
{
const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + height + "\n";
size_t n = 0;
for (size_t p = gcode.find(tag); p != std::string::npos; p = gcode.find(tag, p + 1))
++n;
return n;
}
TEST_CASE("Combining sparse layers folds a run into whole layers the nozzle can lay down", "[WipeTower]")
{
// 0.1 mm layers under a 0.32 mm cap: three fit (0.3), a fourth does not, so a run prints once
// every three layers at 0.3 mm.
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.1, "0.32", false));
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.1, "0.32", true));
REQUIRE(plain.planned == combined.planned); // the plan still has one layer per object layer
REQUIRE(plain.sparse > 10);
CHECK(plain.folded == 0);
CHECK_THAT(plain.tallest_printed, Catch::Matchers::WithinAbs(0.1f, 1e-4f));
CHECK(combined.folded > 0);
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.3f, 1e-4f));
// Two of every three sparse layers fold away, leaving the toolchange layers untouched.
CHECK(combined.folded <= plain.sparse);
CHECK(combined.folded >= plain.sparse / 2);
// What folds away comes back as height on the layer that prints the run: no gap, nothing twice.
CHECK_THAT(combined.printed_height, Catch::Matchers::WithinAbs(plain.printed_height, 1e-3f));
}
TEST_CASE("A run too thin to reach the nozzle's layer height is left alone", "[WipeTower]")
{
// Only whole layers merge, so two 0.2 mm layers (0.4) do not fit a 0.32 mm maximum and the tower
// prints as if the option were off. This is the common 0.4 nozzle case; the tooltip says so.
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.32", false));
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.32", true));
REQUIRE(plain.sparse > 10);
CHECK(combined.folded == 0);
CHECK(combined.planned == plain.planned);
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.2f, 1e-4f));
}
TEST_CASE("A merged tower layer declares its own height to the G-code processor", "[WipeTower]")
{
// Each writer declares a height in a hardcoded tag dialect while the processor reads only its
// printer's, so one of them is always dropped. A merged layer is the first time that shows, as a
// thick layer drawn and costed as a thin one. 0.2 mm layers under a 0.42 mm maximum merge in pairs.
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.42", false));
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.42", true));
REQUIRE(combined.folded > 0);
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.4f, 1e-4f));
// Every layer that prints a merged run has to say so, and nothing may say so without the option.
CHECK(count_height_tags(combined.gcode, "0.4") - count_height_tags(plain.gcode, "0.4") == combined.folded);
}
TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]") TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]")
{ {
// Wrapping detection prints a tower on a plate that purges one filament. Neither the old // Wrapping detection prints a tower on a plate that purges one filament. Neither the old
+92
View File
@@ -278,6 +278,98 @@ TEST_CASE("Only the keep-out ring an object is measured against is drawn", "[Wip
CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02)); CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02));
} }
// ---------------------------------------------------------------------------------------------
// "Combine sparse layers": folding a run of toolchange-free layers into one thicker tower layer.
// ---------------------------------------------------------------------------------------------
TEST_CASE("Sparse layers are combined only when every layer is still the tower's to place", "[WipeTower][CombineSparseLayers]") {
PrintConfig cfg;
cfg.timelapse_type.value = TimelapseType::tlTraditional;
cfg.enable_wrapping_detection.value = false;
cfg.wipe_tower_no_sparse_layers.value = false;
cfg.wipe_tower_sparse_layers_combination.value = false;
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
cfg.wipe_tower_sparse_layers_combination.value = true;
CHECK(wipe_tower_sparse_layers_combined(cfg));
// Dropping the sparse layers outright leaves nothing to combine.
cfg.wipe_tower_no_sparse_layers.value = true;
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
CHECK(wipe_tower_sparse_layers_skipped(cfg));
cfg.wipe_tower_no_sparse_layers.value = false;
// Both of these park the nozzle on the tower every layer, so no layer may be folded away.
cfg.timelapse_type.value = TimelapseType::tlSmooth;
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
cfg.timelapse_type.value = TimelapseType::tlTraditional;
cfg.enable_wrapping_detection.value = true;
CHECK_FALSE(wipe_tower_sparse_layers_combined(cfg));
}
TEST_CASE("A layer folded into a later one is marked on the results the emitter reads", "[WipeTower][CombineSparseLayers]") {
WipeTower::ToolChangeResult folded = make_tcr(1, 1, 0.2f);
folded.combined_away = true;
CHECK(wipe_tower_layer_is_combined_away({folded}));
CHECK_FALSE(wipe_tower_layer_is_combined_away({make_tcr(1, 1, 0.2f)}));
CHECK_FALSE(wipe_tower_layer_is_combined_away({}));
}
TEST_CASE("A run of sparse layers prints once, on its last layer, at the height it covers", "[WipeTower][CombineSparseLayers]") {
// Eight 0.1 mm layers on a 0.3 mm cap: a toolchange on the first and the last, sparse between.
std::vector<float> heights(8, 0.1f);
const std::vector<char> sparse{0, 1, 1, 1, 1, 1, 1, 0};
const std::vector<float> caps(8, 0.3f);
const std::vector<char> combined = combine_sparse_wipe_tower_layers(heights, sparse, caps, 0);
REQUIRE(combined.size() == heights.size());
// Three layers fill the cap exactly: the run flushes on layers 3 and 6, the two below each go.
CHECK(combined == std::vector<char>{0, 1, 1, 0, 1, 1, 0, 0});
CHECK_THAT(heights[3], WithinAbs(0.3f, 1e-5f));
CHECK_THAT(heights[6], WithinAbs(0.3f, 1e-5f));
// Layers that print keep the object covered: nothing is lost and nothing is printed twice.
float printed = 0.f;
for (size_t i = 0; i < heights.size(); ++i)
if (! combined[i])
printed += heights[i];
CHECK_THAT(printed, WithinAbs(0.8f, 1e-5f));
// A toolchange has to purge at its own z, so those layers are left exactly as planned.
CHECK_THAT(heights[0], WithinAbs(0.1f, 1e-5f));
CHECK_THAT(heights[7], WithinAbs(0.1f, 1e-5f));
}
TEST_CASE("The maximum layer height of the nozzle that prints the run caps the merge", "[WipeTower][CombineSparseLayers]") {
// The cap that counts belongs to the layer that prints the run; one that prints nothing lays
// nothing down, so its own cap cannot constrain it. Five 0.1 mm layers, sparse above the first,
// layer 3's nozzle taking only 0.15. (A real run holds one filament, so this only tests the
// look-ahead.)
std::vector<float> heights(5, 0.1f);
std::vector<float> caps(5, 0.3f);
caps[3] = 0.15f;
const std::vector<char> combined = combine_sparse_wipe_tower_layers(heights, {0, 1, 1, 1, 1}, caps, 0);
// Layer 2 cannot hand its 0.2 mm on to layer 3, so it prints there and a fresh run starts above.
CHECK(combined == std::vector<char>{0, 1, 0, 1, 0});
CHECK_THAT(heights[2], WithinAbs(0.2f, 1e-5f));
CHECK_THAT(heights[4], WithinAbs(0.2f, 1e-5f));
// A single layer already past the cap is printed as planned rather than shrunk.
std::vector<float> tall{0.2f, 0.4f, 0.4f};
const std::vector<char> tall_combined = combine_sparse_wipe_tower_layers(tall, {0, 1, 1}, {0.3f, 0.3f, 0.3f}, 0);
CHECK(tall_combined == std::vector<char>{0, 0, 0});
CHECK_THAT(tall[1], WithinAbs(0.4f, 1e-5f));
}
TEST_CASE("The tower's first layer is never folded away", "[WipeTower][CombineSparseLayers]") {
// It carries the brim and has to sit on the bed, however little it purges.
std::vector<float> heights(4, 0.1f);
const std::vector<char> combined = combine_sparse_wipe_tower_layers(heights, {1, 1, 1, 1}, std::vector<float>(4, 0.5f), 0);
CHECK(combined.front() == 0);
CHECK_THAT(heights.front(), WithinAbs(0.1f, 1e-5f));
// Everything above it merges into the top layer, which the cap still fits.
CHECK(combined == std::vector<char>{0, 1, 1, 0});
CHECK_THAT(heights.back(), WithinAbs(0.3f, 1e-5f));
}
TEST_CASE("Footprint padding covers the brim and the extrusion half width on each side", "[WipeTower][NoSparseLayers]") { TEST_CASE("Footprint padding covers the brim and the extrusion half width on each side", "[WipeTower][NoSparseLayers]") {
// A nominal outline hulls extrusion centre lines and is re-centred once the real wall is known, // A nominal outline hulls extrusion centre lines and is re-centred once the real wall is known,
// so a line width per side on top of the brim is what keeps an estimate enclosing the real tower. // so a line width per side on top of the brim is what keeps an estimate enclosing the real tower.
+14
View File
@@ -120,6 +120,20 @@ TEST_CASE("Only modified or non-printable chords qualify as menu accelerators",
CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty()); CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty());
} }
TEST_CASE("Chords the desktop keeps for itself are recognized", "[Shortcuts]")
{
#ifdef _WIN32
CHECK(KeyChord{ WXK_F4, wxMOD_ALT }.is_system_shortcut());
CHECK(KeyChord{ WXK_SPACE, wxMOD_ALT }.is_system_shortcut());
#else
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_ALT }.is_system_shortcut());
CHECK_FALSE(KeyChord{ WXK_SPACE, wxMOD_ALT }.is_system_shortcut());
#endif
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_ALT | wxMOD_SHIFT }.is_system_shortcut());
CHECK_FALSE(KeyChord{ WXK_F4, wxMOD_CONTROL }.is_system_shortcut());
CHECK_FALSE(KeyChord{ WXK_SPACE }.is_system_shortcut());
}
TEST_CASE("Chords convert to wx accelerator entries", "[Shortcuts]") TEST_CASE("Chords convert to wx accelerator entries", "[Shortcuts]")
{ {
const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42); const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42);