mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 11:21:30 +00:00
Compare commits
27
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
35dc1ca6ef | ||
|
|
7f30eb8a49 | ||
|
|
b5d1866445 | ||
|
|
3b1a5c0630 | ||
|
|
18867094fa | ||
|
|
5ae88ae5c7 | ||
|
|
ea3a40d073 | ||
|
|
806568287c | ||
|
|
0342ddbe26 | ||
|
|
33af0651b7 | ||
|
|
781a5d8516 | ||
|
|
38446ab77a | ||
|
|
9080269f6f | ||
|
|
d976d9eb0c | ||
|
|
ae27a795b6 | ||
|
|
ecd0be353c | ||
|
|
6a07853933 | ||
|
|
5ae9015c82 | ||
|
|
0ddc730854 | ||
|
|
ea280ba6f6 | ||
|
|
237cd10eb5 | ||
|
|
e77d179bbe | ||
|
|
d5aaa463c8 | ||
|
|
8c03985818 | ||
|
|
93b58a2034 | ||
|
|
6be6fdd7c7 | ||
|
|
521a30a45c |
@@ -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')"
|
||||||
|
else
|
||||||
|
changed_files="$(git log --since="$since" --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
|
||||||
|
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
|
||||||
|
| sed '/^$/d')"
|
||||||
|
fi
|
||||||
|
|
||||||
if [ -n "$changed_files" ]; then
|
if [ -n "$changed_files" ]; then
|
||||||
echo "OFL changed on $branch since $since:"
|
echo "OFL changed on $branch from ${since:-the beginning} through $SCAN_UNTIL:"
|
||||||
echo "$changed_files"
|
echo "$changed_files"
|
||||||
changed=true
|
changed=true
|
||||||
else
|
else
|
||||||
echo "No OFL changes on $branch since $since."
|
echo "No OFL changes on $branch through $SCAN_UNTIL."
|
||||||
changed=false
|
changed=false
|
||||||
fi
|
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
@@ -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")
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Vendored
+2
-2
@@ -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}")
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
|
||||||
|
|||||||
@@ -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.
|
||||||
+45
-19
@@ -28,8 +28,8 @@ Per-vendor granularity is what makes the system practical:
|
|||||||
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
|
- A vendor whose profile is bumped invalidates only its own cache. The other 60-odd
|
||||||
vendors keep theirs — even when the bumped vendor is the shared Orca filament
|
vendors keep theirs — even when the bumped vendor is the shared Orca filament
|
||||||
library everyone else inherits from.
|
library everyone else inherits from.
|
||||||
- The setup wizard, which loads vendors one at a time, gets the same speedup as
|
- The setup wizard loads its vendors through the same routine as startup, so it
|
||||||
startup without a second code path.
|
gets the same speedup without a second code path.
|
||||||
- A vendor with no cache, or a broken one, costs only that vendor a parse.
|
- A vendor with no cache, or a broken one, costs only that vendor a parse.
|
||||||
|
|
||||||
A cache holds *system* presets only. User presets, project settings and modified
|
A cache holds *system* presets only. User presets, project settings and modified
|
||||||
@@ -162,15 +162,20 @@ cache nothing can invalidate is worse than no cache.
|
|||||||
|
|
||||||
Vendors load in a fixed order, because filament inheritance crosses exactly one
|
Vendors load in a fixed order, because filament inheritance crosses exactly one
|
||||||
boundary: any vendor's filament may inherit from the shared Orca filament library,
|
boundary: any vendor's filament may inherit from the shared Orca filament library,
|
||||||
and nothing else reaches across vendors — an `include` is always vendor-local. The
|
and nothing else reaches across vendors — an `include` is always vendor-local. Only
|
||||||
library therefore goes first, alone; every other vendor follows in parallel, resolving
|
installing a vendor's presets crosses it; reading the vendor, from its cache or its
|
||||||
against it; and the results are merged in a stable order:
|
JSONs, needs nothing from the library. So every other vendor is read while the library
|
||||||
|
loads, each is installed against it as soon as both are done, and the results are
|
||||||
|
merged in a stable order:
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
lib["1 · OrcaFilamentLibrary<br/>loaded first, synchronously"] --> par["2 · every other vendor in parallel,<br/>each into its own bundle, filaments<br/>resolving against the loaded library"] --> merge["3 · bundles merged into one,<br/>sequentially, in stable vendor order"]
|
lib["1 · OrcaFilamentLibrary loaded;<br/>meanwhile every other vendor read<br/>from its cache or its JSONs"] --> par["2 · every other vendor installed<br/>in parallel, each into its own bundle,<br/>filaments resolving against the library"] --> merge["3 · bundles merged into one,<br/>in one pass per collection,<br/>in stable vendor order"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`PresetBundle::load_vendors` runs these steps for startup and for the setup wizard,
|
||||||
|
which hand it the vendors to load and the directory each is installed in.
|
||||||
|
|
||||||
Whether a vendor comes from its cache or from a parse changes nothing in that
|
Whether a vendor comes from its cache or from a parse changes nothing in that
|
||||||
order — both produce the same bundle, so cached and parsed vendors mix freely in
|
order — both produce the same bundle, so cached and parsed vendors mix freely in
|
||||||
one startup.
|
one startup.
|
||||||
@@ -211,11 +216,12 @@ shipped cache answered first, so the profile in `<data_dir>/system/` was never p
|
|||||||
and its cache was never written back.
|
and its cache was never written back.
|
||||||
|
|
||||||
Serving from a cache is not a memory-image restore. The entries are deserialized and
|
Serving from a cache is not a memory-image restore. The entries are deserialized and
|
||||||
then installed one by one — inheritance resolved against the presets installed before
|
then installed by `install_vendor`, the routine the JSON path hands the vendor's entries
|
||||||
them and the currently loaded filament library, includes layered in, configs flattened
|
to once it has parsed the sub-files: inheritance resolved against the presets installed
|
||||||
onto the collection defaults, validated and registered — by the same function the JSON
|
before them and the currently loaded filament library, includes layered in, configs
|
||||||
path calls straight after parsing a sub-file. An `include` layers what the included
|
flattened onto the collection defaults, validated and registered. An `include` layers
|
||||||
base states, between the parent and the preset's own keys: the base's diff against the
|
what the included base states, between the parent and the preset's own keys: the base's
|
||||||
|
diff against the
|
||||||
default, taken when the base itself was installed and before the per-variant padding
|
default, taken when the base itself was installed and before the per-variant padding
|
||||||
`inherits` sees, so only what a template sets reaches the presets including it. The two
|
`inherits` sees, so only what a template sets reaches the presets including it. The two
|
||||||
paths share everything below the parse, which is what makes a cache-loaded bundle
|
paths share everything below the parse, which is what makes a cache-loaded bundle
|
||||||
@@ -223,6 +229,16 @@ indistinguishable from a JSON-loaded one by construction rather than by test cov
|
|||||||
Installation also rebuilds each preset's file path from the local data directory, so a
|
Installation also rebuilds each preset's file path from the local data directory, so a
|
||||||
shipped cache never carries the generating machine's paths.
|
shipped cache never carries the generating machine's paths.
|
||||||
|
|
||||||
|
Installing an entry is split in two. `resolve_vendor_preset` flattens it, reading only
|
||||||
|
what is registered under the names it inherits and includes, and `commit_vendor_preset`
|
||||||
|
registers it, the only step that writes anything shared. Entries resolve across threads
|
||||||
|
in runs and commit in the order the vendor lists them. A run ends before an entry that
|
||||||
|
inherits or includes one already in it, since that one's commit registers what the
|
||||||
|
entry resolves against, so no entry in a run reads what another in it registers. An
|
||||||
|
entry's parse messages are held until it commits. The bundle, the log's parse and
|
||||||
|
install messages and the error count therefore come out as parsing and installing one
|
||||||
|
entry at a time would leave them, whatever the listing order.
|
||||||
|
|
||||||
App upgrades work because a cache normally survives one. Only a deliberate
|
App upgrades work because a cache normally survives one. Only a deliberate
|
||||||
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
|
`CACHE_VERSION` bump makes an installed cache unreadable, and that is handled at
|
||||||
install time rather than at load: a vendor whose cache this build cannot read counts
|
install time rather than at load: a vendor whose cache this build cannot read counts
|
||||||
@@ -254,17 +270,20 @@ the wizard caches the *derived JSON*, not another form of the inputs:
|
|||||||
open, the wizard computes the current stamps (one version peek per vendor) and, when
|
open, the wizard computes the current stamps (one version peek per vendor) and, when
|
||||||
they match, serves the catalog from the file — no bundle built, no preset installed.
|
they match, serves the catalog from the file — no bundle built, no preset installed.
|
||||||
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
|
Caching bundle inputs instead was tried and measured: rebuilding the bundle from
|
||||||
per-vendor caches costs ~2 s of preset installation whatever feeds it, so only
|
per-vendor caches costs over a second of preset installation whatever feeds it, so
|
||||||
skipping the rebuild entirely wins.
|
only skipping the rebuild entirely wins.
|
||||||
|
|
||||||
Any change to the set — a vendor added, removed or updated, or its cache-only
|
Any change to the set — a vendor added, removed or updated, or its cache-only
|
||||||
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
|
`.opc` replaced by a newer one — changes the stamps and retires the whole file;
|
||||||
the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where
|
the wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load
|
||||||
they cover) and writes the catalog back. Selections, region and per-open decorations
|
startup uses (per-vendor caches serving where they cover), and writes the catalog
|
||||||
are applied downstream of the cache either way, so a served catalog is
|
back. When a vendor fails to load, the filament library included, that open falls
|
||||||
indistinguishable from a rebuilt one. Nothing ships this file and the updater never
|
back to the wizard's own scan of the vendor JSONs, as when no bundle can be built,
|
||||||
touches it; it is a locally written artifact, re-derived whenever stale, written
|
and writes nothing. Selections, region and per-open decorations are applied
|
||||||
through a temp file and rename so half a cache is never readable.
|
downstream of the cache either way, so a served catalog is indistinguishable from a
|
||||||
|
rebuilt one. Nothing ships this file and the updater never touches it; it is a
|
||||||
|
locally written artifact, re-derived whenever stale, written through a temp file and
|
||||||
|
rename so half a cache is never readable.
|
||||||
|
|
||||||
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
|
The cache lives under `<data_dir>/cache/`, not beside the vendors: everything that
|
||||||
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
|
scans `<data_dir>/system/` treats any `.opc` there as a vendor, so a non-vendor
|
||||||
@@ -378,6 +397,13 @@ enumerates only `*.json` will find no vendors at all in a packaged build.
|
|||||||
the `CachedPreset` field list — written and read by `visit_entry` in
|
the `CachedPreset` field list — written and read by `visit_entry` in
|
||||||
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
|
`PresetCacheFormat.cpp`, one list for the save, the load and the name peek alike — or
|
||||||
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
|
the cache's own layout or stamps, requires bumping `CACHE_VERSION` by hand.
|
||||||
|
- **Adding a kind of reference between presets**, as `inherits` and `include` are:
|
||||||
|
parse the names into `CachedPreset` (a field change, so `CACHE_VERSION` is bumped),
|
||||||
|
have `install_vendor_entries` end a run before an entry that names one already in it
|
||||||
|
and retain what the names point at, look them up only in `resolve_vendor_preset`, and
|
||||||
|
register what they point at only in `commit_vendor_preset`. The listing-order test in
|
||||||
|
`test_vendor_cache.cpp` fails for a kind the runs do not check once its fixture uses
|
||||||
|
it.
|
||||||
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
|
- **The dictionary indexes with a `uint16`**, so `print_config_def` may hold at most
|
||||||
65535 options and one cache at most 65535 distinct enum value names.
|
65535 options and one cache at most 65535 distinct enum value names.
|
||||||
`CacheDictionary::save` throws past that, which surfaces when CI generates the
|
`CacheDictionary::save` throws past that, which surfaces when CI generates the
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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>
|
||||||
|
|||||||
+46
-64
@@ -5,6 +5,7 @@
|
|||||||
//BBS
|
//BBS
|
||||||
#include "Preset.hpp"
|
#include "Preset.hpp"
|
||||||
#include "Exception.hpp"
|
#include "Exception.hpp"
|
||||||
|
#include "InstanceLock.hpp"
|
||||||
#include "LocalesUtils.hpp"
|
#include "LocalesUtils.hpp"
|
||||||
#include "Thread.hpp"
|
#include "Thread.hpp"
|
||||||
#include "format.hpp"
|
#include "format.hpp"
|
||||||
@@ -739,10 +740,13 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs)
|
|||||||
|
|
||||||
|
|
||||||
#ifdef USE_JSON_CONFIG
|
#ifdef USE_JSON_CONFIG
|
||||||
std::string AppConfig::load()
|
std::string AppConfig::load(bool read_only)
|
||||||
{
|
{
|
||||||
json j;
|
json j;
|
||||||
|
|
||||||
|
// Keep another instance from replacing or restoring the file mid-read.
|
||||||
|
InstanceLock instance_lock(read_only ? std::string() : lock_path());
|
||||||
|
|
||||||
// 1) Read the complete config file into a boost::property_tree.
|
// 1) Read the complete config file into a boost::property_tree.
|
||||||
namespace pt = boost::property_tree;
|
namespace pt = boost::property_tree;
|
||||||
pt::ptree tree;
|
pt::ptree tree;
|
||||||
@@ -992,7 +996,6 @@ void AppConfig::save()
|
|||||||
// The config is first written to a file with a PID suffix and then moved
|
// The config is first written to a file with a PID suffix and then moved
|
||||||
// to avoid race conditions with multiple instances of Slic3r
|
// to avoid race conditions with multiple instances of Slic3r
|
||||||
const auto path = config_path();
|
const auto path = config_path();
|
||||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
|
||||||
|
|
||||||
json j;
|
json j;
|
||||||
|
|
||||||
@@ -1122,43 +1125,18 @@ void AppConfig::save()
|
|||||||
|
|
||||||
j["local_machines"][local_machine.first] = m_json;
|
j["local_machines"][local_machine.first] = m_json;
|
||||||
}
|
}
|
||||||
boost::nowide::ofstream c;
|
const std::string config_str = j.dump(1, '\t');
|
||||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
if (write_config_file(path, config_str + "\n", config_str))
|
||||||
c << j.dump(1, '\t') << std::endl;
|
|
||||||
|
|
||||||
#ifdef WIN32
|
|
||||||
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
|
|
||||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
|
||||||
// we save the config file into a backup first before moving it to the final destination.
|
|
||||||
c << appconfig_md5_hash_line(j.dump(1, '\t'));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
c.close();
|
|
||||||
if (c.fail()) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef WIN32
|
|
||||||
// Make a backup of the configuration file before copying it to the final destination.
|
|
||||||
std::string error_message;
|
|
||||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
|
||||||
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
|
|
||||||
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Rename the config atomically.
|
|
||||||
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
|
|
||||||
// To cope with that, we already made a backup of the config on Windows.
|
|
||||||
rename_file(path_pid, path);
|
|
||||||
m_dirty = false;
|
m_dirty = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
|
|
||||||
std::string AppConfig::load()
|
std::string AppConfig::load(bool read_only)
|
||||||
{
|
{
|
||||||
|
// Keep another instance from replacing or restoring the file mid-read.
|
||||||
|
InstanceLock instance_lock(read_only ? std::string() : lock_path());
|
||||||
|
|
||||||
// 1) Read the complete config file into a boost::property_tree.
|
// 1) Read the complete config file into a boost::property_tree.
|
||||||
namespace pt = boost::property_tree;
|
namespace pt = boost::property_tree;
|
||||||
pt::ptree tree;
|
pt::ptree tree;
|
||||||
@@ -1296,7 +1274,6 @@ void AppConfig::save()
|
|||||||
// The config is first written to a file with a PID suffix and then moved
|
// The config is first written to a file with a PID suffix and then moved
|
||||||
// to avoid race conditions with multiple instances of Slic3r
|
// to avoid race conditions with multiple instances of Slic3r
|
||||||
const auto path = config_path();
|
const auto path = config_path();
|
||||||
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
|
||||||
|
|
||||||
std::stringstream config_ss;
|
std::stringstream config_ss;
|
||||||
if (m_mode == EAppMode::Editor)
|
if (m_mode == EAppMode::Editor)
|
||||||
@@ -1332,39 +1309,39 @@ void AppConfig::save()
|
|||||||
// One empty line before the MD5 sum.
|
// One empty line before the MD5 sum.
|
||||||
config_ss << std::endl;
|
config_ss << std::endl;
|
||||||
|
|
||||||
std::string config_str = config_ss.str();
|
const std::string config_str = config_ss.str();
|
||||||
boost::nowide::ofstream c;
|
if (write_config_file(path, config_str, config_str))
|
||||||
c.open(path_pid, std::ios::out | std::ios::trunc);
|
|
||||||
c << config_str;
|
|
||||||
#ifdef WIN32
|
|
||||||
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
|
|
||||||
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
|
||||||
// we save the config file into a backup first before moving it to the final destination.
|
|
||||||
c << appconfig_md5_hash_line(config_str);
|
|
||||||
#endif
|
|
||||||
c.close();
|
|
||||||
if (c.fail()) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef WIN32
|
|
||||||
// Make a backup of the configuration file before copying it to the final destination.
|
|
||||||
std::string error_message;
|
|
||||||
std::string backup_path = (boost::format("%1%.bak") % path).str();
|
|
||||||
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
|
|
||||||
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Rename the config atomically.
|
|
||||||
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
|
|
||||||
// To cope with that, we already made a backup of the config on Windows.
|
|
||||||
rename_file(path_pid, path);
|
|
||||||
m_dirty = false;
|
m_dirty = false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
bool AppConfig::write_config_file(const std::string &path, std::string body, const std::string &checksum_source)
|
||||||
|
{
|
||||||
|
// Everything before this is assembly; only the writes need the other instances kept out.
|
||||||
|
InstanceLock instance_lock(lock_path());
|
||||||
|
#ifdef WIN32
|
||||||
|
// WIN32 specific: the final replace is not safe in case of an application crash, there is no atomic "rename file" API
|
||||||
|
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
|
||||||
|
// we save the config file into a backup first before moving it to the final destination.
|
||||||
|
body += appconfig_md5_hash_line(checksum_source);
|
||||||
|
#endif
|
||||||
|
// Not flushed to the device: the idle handler saves on the GUI thread after
|
||||||
|
// any change, and the rename already gives a complete old or new file.
|
||||||
|
if (const std::error_code ec = write_file_atomically(path, body)) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Failed to write the configuration " << path << ": " << ec.message() << "; trying again in 10 s";
|
||||||
|
m_retry_save_at = std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
m_retry_save_at = {};
|
||||||
|
#ifdef WIN32
|
||||||
|
// Written after the config, so the backup never holds a state that was not confirmed written.
|
||||||
|
const std::string backup_path = (boost::format("%1%.bak") % path).str();
|
||||||
|
if (const std::error_code ec = write_file_atomically(backup_path, body))
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Failed to write the backup configuration " << backup_path << ": " << ec.message();
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
||||||
{
|
{
|
||||||
const auto it_v = m_vendors.find(vendor);
|
const auto it_v = m_vendors.find(vendor);
|
||||||
@@ -1853,6 +1830,11 @@ void AppConfig::reset_selections()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string AppConfig::lock_path()
|
||||||
|
{
|
||||||
|
return Slic3r::data_dir().empty() ? std::string() : config_path() + ".lock";
|
||||||
|
}
|
||||||
|
|
||||||
std::string AppConfig::config_path()
|
std::string AppConfig::config_path()
|
||||||
{
|
{
|
||||||
#ifdef USE_JSON_CONFIG
|
#ifdef USE_JSON_CONFIG
|
||||||
@@ -1889,7 +1871,7 @@ bool AppConfig::exists()
|
|||||||
|
|
||||||
std::string AppConfig::load_if_exists()
|
std::string AppConfig::load_if_exists()
|
||||||
{
|
{
|
||||||
return boost::filesystem::exists(loading_path()) ? load() : std::string();
|
return boost::filesystem::exists(loading_path()) ? load(/*read_only=*/true) : std::string();
|
||||||
}
|
}
|
||||||
|
|
||||||
}; // namespace Slic3r
|
}; // namespace Slic3r
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
#define slic3r_AppConfig_hpp_
|
#define slic3r_AppConfig_hpp_
|
||||||
|
|
||||||
#include <set>
|
#include <set>
|
||||||
|
#include <chrono>
|
||||||
#include <map>
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include "nlohmann/json.hpp"
|
#include "nlohmann/json.hpp"
|
||||||
@@ -122,14 +123,18 @@ public:
|
|||||||
|
|
||||||
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
|
||||||
// Return an error string, or an empty string on success.
|
// Return an error string, or an empty string on success.
|
||||||
std::string load();
|
std::string load(bool read_only = false);
|
||||||
// Treat a missing config as default state; otherwise load it normally.
|
// Treat a missing config as default state; otherwise load it normally.
|
||||||
|
// The CLI's load: it never saves, so it takes no lock and creates no lock file.
|
||||||
std::string load_if_exists();
|
std::string load_if_exists();
|
||||||
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
|
||||||
void save();
|
void save();
|
||||||
|
|
||||||
// Does this config need to be saved?
|
// Does this config need to be saved?
|
||||||
bool dirty() const { return m_dirty; }
|
bool dirty() const { return m_dirty; }
|
||||||
|
// False for ten seconds after a failed write, so the idle handler does not
|
||||||
|
// repeat a hopeless attempt on every event; an explicit save() always tries.
|
||||||
|
bool save_due() const { return std::chrono::steady_clock::now() >= m_retry_save_at; }
|
||||||
|
|
||||||
|
|
||||||
void set_dirty() { m_dirty = true; }
|
void set_dirty() { m_dirty = true; }
|
||||||
@@ -339,6 +344,8 @@ public:
|
|||||||
|
|
||||||
// Get the default config path from Slic3r::data_dir().
|
// Get the default config path from Slic3r::data_dir().
|
||||||
std::string config_path();
|
std::string config_path();
|
||||||
|
// Lock file guarding config_path() against other running instances; empty without a data dir.
|
||||||
|
std::string lock_path();
|
||||||
|
|
||||||
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
|
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
|
||||||
bool legacy_datadir() const { return m_legacy_datadir; }
|
bool legacy_datadir() const { return m_legacy_datadir; }
|
||||||
@@ -449,8 +456,16 @@ private:
|
|||||||
|
|
||||||
// Preset for each machine
|
// Preset for each machine
|
||||||
MachineSettingMap m_printer_settings;
|
MachineSettingMap m_printer_settings;
|
||||||
|
// Writes the assembled config text, and on Windows its checksum and a backup copy; false when the
|
||||||
|
// config itself could not be written, in which case the caller stays dirty and retries. `checksum_source`
|
||||||
|
// is the text load() will verify, which for the JSON config ends before the trailing newline.
|
||||||
|
bool write_config_file(const std::string &path, std::string body, const std::string &checksum_source);
|
||||||
|
|
||||||
// Has any value been modified since the config.ini has been last saved or loaded?
|
// Has any value been modified since the config.ini has been last saved or loaded?
|
||||||
bool m_dirty;
|
bool m_dirty;
|
||||||
|
// After a failed write, save_due() is false for the next ten seconds, so the
|
||||||
|
// idle handler does not repeat a hopeless write on every event.
|
||||||
|
std::chrono::steady_clock::time_point m_retry_save_at{};
|
||||||
// Original version found in the ini file before it was overwritten
|
// Original version found in the ini file before it was overwritten
|
||||||
Semver m_orig_version;
|
Semver m_orig_version;
|
||||||
// Whether the existing version is before system profiles & configuration updating
|
// Whether the existing version is before system profiles & configuration updating
|
||||||
|
|||||||
@@ -304,6 +304,8 @@ set(lisbslic3r_sources
|
|||||||
Geometry/VoronoiUtils.cpp
|
Geometry/VoronoiUtils.cpp
|
||||||
Geometry/VoronoiUtils.hpp
|
Geometry/VoronoiUtils.hpp
|
||||||
Geometry/VoronoiVisualUtils.hpp
|
Geometry/VoronoiVisualUtils.hpp
|
||||||
|
InstanceLock.cpp
|
||||||
|
InstanceLock.hpp
|
||||||
Int128.hpp
|
Int128.hpp
|
||||||
KDTreeIndirect.hpp
|
KDTreeIndirect.hpp
|
||||||
Layer.cpp
|
Layer.cpp
|
||||||
@@ -353,6 +355,7 @@ set(lisbslic3r_sources
|
|||||||
Optimize/Optimizer.hpp
|
Optimize/Optimizer.hpp
|
||||||
Orient.cpp
|
Orient.cpp
|
||||||
Orient.hpp
|
Orient.hpp
|
||||||
|
ParallelResolve.hpp
|
||||||
ParameterUtils.cpp
|
ParameterUtils.cpp
|
||||||
ParameterUtils.hpp
|
ParameterUtils.hpp
|
||||||
pchheader.cpp
|
pchheader.cpp
|
||||||
|
|||||||
+29
-18
@@ -849,6 +849,19 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
|
|||||||
return std::move(substitutions_ctxt.substitutions);
|
return std::move(substitutions_ctxt.substitutions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Case-insensitive compare of a JSON key against a fixed ASCII one, without
|
||||||
|
// boost::iequals, whose std::locale() takes a lock the whole process shares in the
|
||||||
|
// MSVC runtime.
|
||||||
|
static bool ascii_iequals(const std::string &key, const char *literal)
|
||||||
|
{
|
||||||
|
auto lower = [](char c) { return (c >= 'A' && c <= 'Z') ? char(c - 'A' + 'a') : c; };
|
||||||
|
size_t i = 0;
|
||||||
|
for (; i < key.size() && literal[i] != '\0'; ++ i)
|
||||||
|
if (lower(key[i]) != lower(literal[i]))
|
||||||
|
return false;
|
||||||
|
return i == key.size() && literal[i] == '\0';
|
||||||
|
}
|
||||||
|
|
||||||
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
|
int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContext& substitution_context, bool load_inherits_to_config, std::map<std::string, std::string>& key_values, std::string& reason)
|
||||||
{
|
{
|
||||||
json j;
|
json j;
|
||||||
@@ -916,44 +929,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
|
|||||||
}
|
}
|
||||||
//parse the json elements
|
//parse the json elements
|
||||||
for (auto it = j.begin(); it != j.end(); it++) {
|
for (auto it = j.begin(); it != j.end(); it++) {
|
||||||
if (boost::iequals(it.key(),BBL_JSON_KEY_VERSION)) {
|
if (ascii_iequals(it.key(), BBL_JSON_KEY_VERSION)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_VERSION, it.value());
|
key_values.emplace(BBL_JSON_KEY_VERSION, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
|
||||||
//skip it
|
//skip it
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_NAME)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_NAME, it.value());
|
key_values.emplace(BBL_JSON_KEY_NAME, it.value());
|
||||||
if (it.value() == "project_settings")
|
if (it.value() == "project_settings")
|
||||||
is_project_settings = true;
|
is_project_settings = true;
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_URL)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_URL)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_URL, it.value());
|
key_values.emplace(BBL_JSON_KEY_URL, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_TYPE)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_TYPE, it.value());
|
key_values.emplace(BBL_JSON_KEY_TYPE, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_SETTING_ID)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value());
|
key_values.emplace(BBL_JSON_KEY_SETTING_ID, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FILAMENT_ID)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value());
|
key_values.emplace(BBL_JSON_KEY_FILAMENT_ID, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_FROM)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_FROM, it.value());
|
key_values.emplace(BBL_JSON_KEY_FROM, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_DESCRIPTION)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value());
|
key_values.emplace(BBL_JSON_KEY_DESCRIPTION, it.value());
|
||||||
}
|
}
|
||||||
else if (boost::iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
else if (ascii_iequals(it.key(), BBL_JSON_KEY_INSTANTIATION)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value());
|
key_values.emplace(BBL_JSON_KEY_INSTANTIATION, it.value());
|
||||||
}
|
}
|
||||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INHERITS)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
|
key_values.emplace(BBL_JSON_KEY_INHERITS, it.value());
|
||||||
}
|
}
|
||||||
else if (!load_inherits_to_config && boost::iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
else if (!load_inherits_to_config && ascii_iequals(it.key(), BBL_JSON_KEY_INCLUDES)) {
|
||||||
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
|
key_values.emplace(BBL_JSON_KEY_INCLUDES, it.value().dump());
|
||||||
} else if (boost::iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
} else if (ascii_iequals(it.key(), ORCA_JSON_KEY_RENAMED_FROM)) {
|
||||||
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
|
key_values.emplace(ORCA_JSON_KEY_RENAMED_FROM, it.value());
|
||||||
} else {
|
} else {
|
||||||
t_config_option_key opt_key = it.key();
|
t_config_option_key opt_key = it.key();
|
||||||
@@ -1525,11 +1538,9 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
|
|||||||
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
||||||
std::ostringstream ss;
|
std::ostringstream ss;
|
||||||
this->save_to_json(ss, name, from, version);
|
this->save_to_json(ss, name, from, version);
|
||||||
boost::nowide::ofstream c;
|
if (const std::error_code ec = write_file_atomically(file, ss.str()))
|
||||||
c.open(file, std::ios::out | std::ios::trunc);
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": failed to save config to %1%: %2%") % file % ec.message();
|
||||||
c << ss.str();
|
else
|
||||||
c.close();
|
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%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());
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 ¶ms,
|
const FillParams ¶ms,
|
||||||
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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ¶
|
|||||||
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶ms)
|
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams ¶ms)
|
||||||
{
|
{
|
||||||
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
@@ -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 ®ion = 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 ®ion = 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.
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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_
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
#include "InstanceLock.hpp"
|
||||||
|
|
||||||
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <system_error>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <boost/nowide/fstream.hpp>
|
||||||
|
#ifdef _WIN32
|
||||||
|
#include <boost/interprocess/sync/file_lock.hpp>
|
||||||
|
#include <boost/nowide/convert.hpp>
|
||||||
|
#else
|
||||||
|
#include <cerrno>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
// LockFileEx, held by this handle alone.
|
||||||
|
using NativeFileLock = boost::interprocess::file_lock;
|
||||||
|
#else
|
||||||
|
// flock(2) rather than an fcntl lock: it belongs to this open file description,
|
||||||
|
// so any other code in the process that opens and closes the lock file, as a
|
||||||
|
// backup or an export walking the data dir might, cannot drop it. An fcntl
|
||||||
|
// lock would go with the first such close.
|
||||||
|
class NativeFileLock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
explicit NativeFileLock(const char *path) : m_fd(::open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0644))
|
||||||
|
{
|
||||||
|
if (m_fd < 0)
|
||||||
|
throw std::system_error(errno, std::generic_category(), path);
|
||||||
|
}
|
||||||
|
~NativeFileLock() { ::close(m_fd); }
|
||||||
|
bool try_lock()
|
||||||
|
{
|
||||||
|
if (::flock(m_fd, LOCK_EX | LOCK_NB) == 0)
|
||||||
|
return true;
|
||||||
|
// A signal (a child exiting, for one) interrupts the call like any other; the caller polls again.
|
||||||
|
if (errno == EWOULDBLOCK || errno == EINTR)
|
||||||
|
return false;
|
||||||
|
throw std::system_error(errno, std::generic_category(), "flock");
|
||||||
|
}
|
||||||
|
void unlock() { ::flock(m_fd, LOCK_UN); }
|
||||||
|
private:
|
||||||
|
int m_fd;
|
||||||
|
};
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// One slot per lock file, shared by every guard in the process: one lock
|
||||||
|
// object per path behind a mutex is what makes the guard re-entrant and safe
|
||||||
|
// to use from the preset sync thread and the GUI thread at once. The lock
|
||||||
|
// file is opened by the outermost guard and closed when it goes, so the file
|
||||||
|
// is never held open between guards: whatever is at the path is what gets
|
||||||
|
// locked, and a data dir can be removed once nothing is saving into it. The
|
||||||
|
// file is kept rather than deleted on release because the lock state lives in
|
||||||
|
// the kernel on the open file, and deleting it would let a third instance
|
||||||
|
// lock a fresh file while the second still holds the old one.
|
||||||
|
struct InstanceLock::Slot
|
||||||
|
{
|
||||||
|
std::recursive_mutex mutex;
|
||||||
|
// Non-null exactly while this process holds the file lock.
|
||||||
|
std::unique_ptr<NativeFileLock> file_lock;
|
||||||
|
int depth{0};
|
||||||
|
// Until this point, after a guard could not open, lock or wait out the
|
||||||
|
// file, guards do not touch it.
|
||||||
|
std::chrono::steady_clock::time_point cooldown_until{};
|
||||||
|
};
|
||||||
|
|
||||||
|
InstanceLock::Slot &InstanceLock::slot_for(const std::string &lock_file_path)
|
||||||
|
{
|
||||||
|
// Never freed: a save during static destruction still needs its slot.
|
||||||
|
static auto *registry_mutex = new std::mutex();
|
||||||
|
static auto *registry = new std::map<std::string, std::unique_ptr<Slot>>();
|
||||||
|
|
||||||
|
std::lock_guard<std::mutex> guard(*registry_mutex);
|
||||||
|
std::unique_ptr<Slot> &slot = (*registry)[lock_file_path];
|
||||||
|
if (! slot)
|
||||||
|
slot = std::make_unique<Slot>();
|
||||||
|
return *slot;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the cool-down. Called with the slot mutex held.
|
||||||
|
void InstanceLock::defer(Slot &slot, const std::string &reason)
|
||||||
|
{
|
||||||
|
slot.cooldown_until = std::chrono::steady_clock::now() + cooldown;
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << reason << "; proceeding without the lock for the next " << cooldown.count() << " ms";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates the lock file if needed and opens it, or starts the cool-down.
|
||||||
|
// Called with the slot mutex held.
|
||||||
|
bool InstanceLock::open_lock_file(Slot &slot, const std::string &lock_file_path)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
#ifdef _WIN32
|
||||||
|
// The lock opens an existing file; created once, on the first miss.
|
||||||
|
const std::wstring wide_path = boost::nowide::widen(lock_file_path);
|
||||||
|
try {
|
||||||
|
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
boost::nowide::ofstream(lock_file_path, std::ios::app).close();
|
||||||
|
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
slot.file_lock = std::make_unique<NativeFileLock>(lock_file_path.c_str());
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
defer(slot, "Cannot open lock file " + lock_file_path + ": " + e.what() + " (check its owner and permissions)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceLock::InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout)
|
||||||
|
{
|
||||||
|
if (lock_file_path.empty())
|
||||||
|
return;
|
||||||
|
m_slot = &slot_for(lock_file_path);
|
||||||
|
m_slot_guard = std::unique_lock<std::recursive_mutex>(m_slot->mutex);
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (m_slot->depth == 0 && now >= m_slot->cooldown_until && open_lock_file(*m_slot, lock_file_path)) {
|
||||||
|
const auto deadline = now + timeout;
|
||||||
|
bool taken = false;
|
||||||
|
for (;;) {
|
||||||
|
try {
|
||||||
|
if ((taken = m_slot->file_lock->try_lock()))
|
||||||
|
break;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
defer(*m_slot, "Cannot lock " + lock_file_path + ": " + e.what());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (std::chrono::steady_clock::now() >= deadline) {
|
||||||
|
defer(*m_slot, "Another instance has held " + lock_file_path + " for over " + std::to_string(timeout.count()) + " ms");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||||
|
}
|
||||||
|
if (! taken)
|
||||||
|
m_slot->file_lock.reset();
|
||||||
|
}
|
||||||
|
// Counted last, so a throw above leaves the slot exactly as it was found.
|
||||||
|
++ m_slot->depth;
|
||||||
|
m_locked = m_slot->file_lock != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
InstanceLock::~InstanceLock()
|
||||||
|
{
|
||||||
|
if (m_slot == nullptr)
|
||||||
|
return;
|
||||||
|
if (-- m_slot->depth == 0 && m_slot->file_lock) {
|
||||||
|
try {
|
||||||
|
m_slot->file_lock->unlock();
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Cannot unlock instance lock: " << e.what();
|
||||||
|
}
|
||||||
|
m_slot->file_lock.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <mutex>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
// Scoped write lock on a file shared by every running instance of the
|
||||||
|
// application, such as the app config or the user preset directory: threads
|
||||||
|
// of this process are serialised through a recursive mutex, other processes
|
||||||
|
// through an advisory OS file lock on `lock_file_path`. The lock file is
|
||||||
|
// created on first use and kept; the OS releases the lock when its holder
|
||||||
|
// exits, so a crashed instance never leaves a stale lock behind.
|
||||||
|
//
|
||||||
|
// Best effort: when the lock file cannot be opened or locked, or another
|
||||||
|
// instance still holds it after `timeout`, the guard keeps only the in-process
|
||||||
|
// mutex, locked() reports false and the write proceeds, since a hung instance
|
||||||
|
// must never block another one from saving. For `cooldown` afterwards guards
|
||||||
|
// leave the file alone. The wait for the in-process mutex is bounded only by
|
||||||
|
// the longest critical section, so a guard covers a few file operations and
|
||||||
|
// nothing slower.
|
||||||
|
//
|
||||||
|
// Lock order: the preset collection mutex may be held when a guard is taken
|
||||||
|
// (set_sync_info_and_save() calls save_info() under it), never the reverse;
|
||||||
|
// that is why the guards sit at the leaf readers and writers and why a guard
|
||||||
|
// must not be added around save_user_presets(), which takes the collection
|
||||||
|
// mutex through delete_preset().
|
||||||
|
class InstanceLock
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// Long against a critical section of milliseconds, short against the GUI
|
||||||
|
// thread, which is where most guards are taken.
|
||||||
|
static constexpr std::chrono::milliseconds default_timeout{1000};
|
||||||
|
// Long enough that a holder stuck in a debugger does not cost a stall per
|
||||||
|
// save; mutable so tests can shorten it.
|
||||||
|
static inline std::chrono::milliseconds cooldown{10000};
|
||||||
|
|
||||||
|
// An empty path makes the guard a no-op.
|
||||||
|
explicit InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout = default_timeout);
|
||||||
|
~InstanceLock();
|
||||||
|
|
||||||
|
InstanceLock(const InstanceLock &) = delete;
|
||||||
|
InstanceLock &operator=(const InstanceLock &) = delete;
|
||||||
|
|
||||||
|
// True while this process holds the cross-process file lock.
|
||||||
|
bool locked() const { return m_locked; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Slot;
|
||||||
|
static Slot &slot_for(const std::string &lock_file_path);
|
||||||
|
static bool open_lock_file(Slot &slot, const std::string &lock_file_path);
|
||||||
|
static void defer(Slot &slot, const std::string &reason);
|
||||||
|
|
||||||
|
Slot *m_slot{nullptr};
|
||||||
|
std::unique_lock<std::recursive_mutex> m_slot_guard;
|
||||||
|
bool m_locked{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -13,8 +13,19 @@
|
|||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
|
||||||
|
// How many setters this thread holds, so the ones nested in another can skip
|
||||||
|
// setlocale, which takes a lock the whole process shares on Windows.
|
||||||
|
static thread_local int s_numeric_locale_depth = 0;
|
||||||
|
|
||||||
CNumericLocalesSetter::CNumericLocalesSetter()
|
CNumericLocalesSetter::CNumericLocalesSetter()
|
||||||
{
|
{
|
||||||
|
// Nested in another setter on this thread, whose "C" the separator check
|
||||||
|
// confirms is still set.
|
||||||
|
if (s_numeric_locale_depth > 0 && is_decimal_separator_point()) {
|
||||||
|
m_nested = true;
|
||||||
|
++ s_numeric_locale_depth;
|
||||||
|
return;
|
||||||
|
}
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
|
||||||
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr);
|
m_orig_numeric_locale = std::setlocale(LC_NUMERIC, nullptr);
|
||||||
@@ -29,12 +40,17 @@ CNumericLocalesSetter::CNumericLocalesSetter()
|
|||||||
m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
|
m_new_locale = newlocale(LC_NUMERIC_MASK, "C", m_new_locale);
|
||||||
uselocale(m_new_locale);
|
uselocale(m_new_locale);
|
||||||
#endif
|
#endif
|
||||||
|
// Counted last, since the destructor does not run for a constructor that throws.
|
||||||
|
++ s_numeric_locale_depth;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
CNumericLocalesSetter::~CNumericLocalesSetter()
|
CNumericLocalesSetter::~CNumericLocalesSetter()
|
||||||
{
|
{
|
||||||
|
-- s_numeric_locale_depth;
|
||||||
|
if (m_nested)
|
||||||
|
return;
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
|
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -19,8 +19,13 @@ class CNumericLocalesSetter {
|
|||||||
public:
|
public:
|
||||||
CNumericLocalesSetter();
|
CNumericLocalesSetter();
|
||||||
~CNumericLocalesSetter();
|
~CNumericLocalesSetter();
|
||||||
|
// A copy would restore the locale twice, and count down once more than up.
|
||||||
|
CNumericLocalesSetter(const CNumericLocalesSetter&) = delete;
|
||||||
|
CNumericLocalesSetter& operator=(const CNumericLocalesSetter&) = delete;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Inside another setter on this thread, which does the setting and restoring.
|
||||||
|
bool m_nested { false };
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
std::string m_orig_numeric_locale;
|
std::string m_orig_numeric_locale;
|
||||||
#else
|
#else
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <utility>
|
||||||
|
#include <variant>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <tbb/blocked_range.h>
|
||||||
|
#include <tbb/parallel_for.h>
|
||||||
|
#include <tbb/task_arena.h>
|
||||||
|
#include <tbb/task_group.h>
|
||||||
|
|
||||||
|
#include "Exception.hpp"
|
||||||
|
|
||||||
|
namespace Slic3r {
|
||||||
|
|
||||||
|
// A batch of resolved presets, each a whole config, is what resolve_then_commit adds
|
||||||
|
// to peak memory, so it stays far below a vendor's preset count and above any core count.
|
||||||
|
inline constexpr size_t resolve_batch_size = 64;
|
||||||
|
|
||||||
|
// Resolve `count` items that do not depend on each other and install them one at
|
||||||
|
// a time.
|
||||||
|
//
|
||||||
|
// `resolve(i)` runs on any thread and must read only, since the items are
|
||||||
|
// resolved side by side. `commit(i, resolved)` is called for every item in index
|
||||||
|
// order, on the calling thread, and is where shared state is written.
|
||||||
|
//
|
||||||
|
// The items are worked through in batches, so what is resolved and held at once
|
||||||
|
// does not grow with `count`. An exception from either callable propagates after
|
||||||
|
// the batches before it have been committed. A cancellation of the caller's task
|
||||||
|
// group, which stops a batch partway without an exception, throws RuntimeError
|
||||||
|
// before that batch commits.
|
||||||
|
//
|
||||||
|
// `ChunkSetup`, when given, is constructed once for each piece of a batch TBB hands
|
||||||
|
// out, for per-thread state a resolve would otherwise set up per item, such as the
|
||||||
|
// C numeric locale, whose setting takes a lock the whole process shares.
|
||||||
|
template<class ChunkSetup = std::monostate, class Resolve, class Commit>
|
||||||
|
void resolve_then_commit(size_t count, Resolve resolve, Commit commit)
|
||||||
|
{
|
||||||
|
using Resolved = std::invoke_result_t<Resolve&, size_t>;
|
||||||
|
|
||||||
|
std::vector<Resolved> resolved(std::min(count, resolve_batch_size));
|
||||||
|
for (size_t first = 0; first < count; first += resolve_batch_size) {
|
||||||
|
const size_t last = std::min(first + resolve_batch_size, count);
|
||||||
|
// Isolated, so a thread waiting on the batch runs none of the caller's other
|
||||||
|
// tasks before finishing it.
|
||||||
|
tbb::this_task_arena::isolate([&] {
|
||||||
|
tbb::parallel_for(tbb::blocked_range<size_t>(first, last),
|
||||||
|
[&](const tbb::blocked_range<size_t>& range) {
|
||||||
|
ChunkSetup setup;
|
||||||
|
(void) setup;
|
||||||
|
for (size_t i = range.begin(); i < range.end(); ++ i)
|
||||||
|
resolved[i - first] = resolve(i);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
if (tbb::is_current_task_group_canceling())
|
||||||
|
throw RuntimeError("resolve_then_commit: canceled before the batch was resolved");
|
||||||
|
for (size_t i = first; i < last; ++ i)
|
||||||
|
commit(i, std::move(resolved[i - first]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Slic3r
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+352
-168
@@ -5,6 +5,8 @@
|
|||||||
#include "Preset.hpp"
|
#include "Preset.hpp"
|
||||||
#include "PresetBundle.hpp"
|
#include "PresetBundle.hpp"
|
||||||
#include "AppConfig.hpp"
|
#include "AppConfig.hpp"
|
||||||
|
#include "LocalesUtils.hpp"
|
||||||
|
#include "ParallelResolve.hpp"
|
||||||
|
|
||||||
#ifdef _MSC_VER
|
#ifdef _MSC_VER
|
||||||
#define WIN32_LEAN_AND_MEAN
|
#define WIN32_LEAN_AND_MEAN
|
||||||
@@ -49,6 +51,9 @@
|
|||||||
#include "libslic3r.h"
|
#include "libslic3r.h"
|
||||||
#include "LifecycleEvents.hpp"
|
#include "LifecycleEvents.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
#include "InstanceLock.hpp"
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
#include "Time.hpp"
|
#include "Time.hpp"
|
||||||
#include "PlaceholderParser.hpp"
|
#include "PlaceholderParser.hpp"
|
||||||
#include "libslic3r/GCode/Thumbnails.hpp"
|
#include "libslic3r/GCode/Thumbnails.hpp"
|
||||||
@@ -105,6 +110,32 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::string user_presets_lock_path(bool read_only)
|
||||||
|
{
|
||||||
|
return read_only || data_dir().empty() ? std::string() : (fs::path(data_dir()) / (PRESET_USER_DIR ".lock")).string();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Removes a preset file the scan could not load, and its .info, under the lock.
|
||||||
|
// The scan passes read_only when it could not take the lock, since the file may
|
||||||
|
// then be another instance's fresh write that it merely raced.
|
||||||
|
static void remove_preset_files(const std::string &preset_file, bool read_only)
|
||||||
|
{
|
||||||
|
if (read_only)
|
||||||
|
return;
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
// A symlink whose target is missing stays, since the target may come back; one
|
||||||
|
// that cannot even be followed is removed like any other unreadable file.
|
||||||
|
auto remove = [](const fs::path &file) {
|
||||||
|
boost::system::error_code ec;
|
||||||
|
if (fs::status(file, ec).type() != fs::file_not_found)
|
||||||
|
fs::remove(file, ec);
|
||||||
|
};
|
||||||
|
fs::path file_path(preset_file);
|
||||||
|
remove(file_path);
|
||||||
|
file_path.replace_extension(".info");
|
||||||
|
remove(file_path);
|
||||||
|
}
|
||||||
|
|
||||||
std::string get_preset_bare_name(const std::string &canonical_name)
|
std::string get_preset_bare_name(const std::string &canonical_name)
|
||||||
{
|
{
|
||||||
const auto pos = canonical_name.find_last_of('/');
|
const auto pos = canonical_name.find_last_of('/');
|
||||||
@@ -506,10 +537,11 @@ void Preset::normalize(DynamicPrintConfig &config)
|
|||||||
handle_legacy_sla(config);
|
handle_legacy_sla(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config)
|
std::string Preset::remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||||
|
const DynamicPrintConfig *added)
|
||||||
{
|
{
|
||||||
std::string incorrect_keys;
|
std::string incorrect_keys;
|
||||||
for (const std::string &key : config.keys())
|
for (const std::string &key : (added != nullptr ? *added : config).keys())
|
||||||
if (! default_config.has(key)) {
|
if (! default_config.has(key)) {
|
||||||
if (incorrect_keys.empty())
|
if (incorrect_keys.empty())
|
||||||
incorrect_keys = key;
|
incorrect_keys = key;
|
||||||
@@ -621,7 +653,6 @@ void Preset::load_info(const std::string& file)
|
|||||||
}
|
}
|
||||||
else if (v.first.compare("base_id") == 0) {
|
else if (v.first.compare("base_id") == 0) {
|
||||||
this->base_id = v.second.get_value<std::string>();
|
this->base_id = v.second.get_value<std::string>();
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << file << " and base_id: " << this->base_id;
|
|
||||||
if (this->base_id.compare("null") == 0)
|
if (this->base_id.compare("null") == 0)
|
||||||
this->base_id.clear();
|
this->base_id.clear();
|
||||||
}
|
}
|
||||||
@@ -647,18 +678,20 @@ void Preset::save_info(std::string file)
|
|||||||
file = idx_file.string();
|
file = idx_file.string();
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::nowide::ofstream c;
|
|
||||||
c.open(file, std::ios::out | std::ios::trunc);
|
|
||||||
std::string sync_info_to_save;
|
std::string sync_info_to_save;
|
||||||
//BBS: hold is used for stop requesting to server this time
|
//BBS: hold is used for stop requesting to server this time
|
||||||
if (this->sync_info.compare("hold") != 0)
|
if (this->sync_info.compare("hold") != 0)
|
||||||
sync_info_to_save = this->sync_info;
|
sync_info_to_save = this->sync_info;
|
||||||
|
std::ostringstream c;
|
||||||
c << "sync_info" << " = " << sync_info_to_save << std::endl;
|
c << "sync_info" << " = " << sync_info_to_save << std::endl;
|
||||||
c << "user_id" << " = " << this->user_id << std::endl;
|
c << "user_id" << " = " << this->user_id << std::endl;
|
||||||
c << "setting_id" << " = " << this->setting_id << std::endl;
|
c << "setting_id" << " = " << this->setting_id << std::endl;
|
||||||
c << "base_id" << " = " << this->base_id << std::endl;
|
c << "base_id" << " = " << this->base_id << std::endl;
|
||||||
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
|
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
|
||||||
c.close();
|
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
if (const std::error_code ec = write_file_atomically(file, c.str()))
|
||||||
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << file << ": " << ec.message();
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preset::remove_files(bool cloud_already_deleted)
|
void Preset::remove_files(bool cloud_already_deleted)
|
||||||
@@ -667,6 +700,7 @@ void Preset::remove_files(bool cloud_already_deleted)
|
|||||||
if (this->is_project_embedded) {
|
if (this->is_project_embedded) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
// Erase the preset file.
|
// Erase the preset file.
|
||||||
boost::nowide::remove(this->file.c_str());
|
boost::nowide::remove(this->file.c_str());
|
||||||
fs::path idx_path(this->file);
|
fs::path idx_path(this->file);
|
||||||
@@ -684,11 +718,11 @@ void Preset::remove_files(bool cloud_already_deleted)
|
|||||||
}
|
}
|
||||||
|
|
||||||
//BBS: add logic for only difference save
|
//BBS: add logic for only difference save
|
||||||
void Preset::save(DynamicPrintConfig* parent_config)
|
bool Preset::save(DynamicPrintConfig* parent_config)
|
||||||
{
|
{
|
||||||
//BBS: add project embedded preset logic
|
//BBS: add project embedded preset logic
|
||||||
if (this->is_project_embedded)
|
if (this->is_project_embedded)
|
||||||
return;
|
return true;
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
//this->config.save(this->file);
|
//this->config.save(this->file);
|
||||||
std::string from_str;
|
std::string from_str;
|
||||||
@@ -703,12 +737,16 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
else
|
else
|
||||||
from_str = std::string("Default");
|
from_str = std::string("Default");
|
||||||
|
|
||||||
boost::filesystem::create_directories(fs::path(this->file).parent_path());
|
|
||||||
const std::string bare_name = get_preset_bare_name(this->name);
|
const std::string bare_name = get_preset_bare_name(this->name);
|
||||||
|
|
||||||
|
// What gets written: the diff against the parent, the config plus its
|
||||||
|
// filament id, or the config as is. Built before the lock is taken so the
|
||||||
|
// exclusive window covers only the file writes.
|
||||||
|
DynamicPrintConfig temp_config;
|
||||||
|
const DynamicPrintConfig *to_save = &this->config;
|
||||||
|
|
||||||
//BBS: only save difference if it has parent
|
//BBS: only save difference if it has parent
|
||||||
if (parent_config) {
|
if (parent_config) {
|
||||||
DynamicPrintConfig temp_config;
|
|
||||||
std::vector<std::string> dirty_options = config.diff(*parent_config);
|
std::vector<std::string> dirty_options = config.diff(*parent_config);
|
||||||
|
|
||||||
std::string extruder_id_name, extruder_variant_name;
|
std::string extruder_id_name, extruder_variant_name;
|
||||||
@@ -744,13 +782,22 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
opt_dst->set(opt_src);
|
opt_dst->set(opt_src);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
to_save = &temp_config;
|
||||||
} else if (!filament_id.empty() && inherits().empty()) {
|
} else if (!filament_id.empty() && inherits().empty()) {
|
||||||
DynamicPrintConfig temp_config = config;
|
temp_config = config;
|
||||||
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
|
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
|
||||||
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
to_save = &temp_config;
|
||||||
} else {
|
}
|
||||||
this->config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
|
|
||||||
|
std::ostringstream json;
|
||||||
|
to_save->save_to_json(json, bare_name, from_str, this->version.to_string());
|
||||||
|
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
boost::filesystem::create_directories(fs::path(this->file).parent_path());
|
||||||
|
if (const std::error_code ec = write_file_atomically(this->file, json.str())) {
|
||||||
|
// No .info either: one without its preset reads as a cloud deletion request.
|
||||||
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << this->file << ": " << ec.message();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
|
||||||
|
|
||||||
@@ -760,6 +807,7 @@ void Preset::save(DynamicPrintConfig* parent_config)
|
|||||||
idx_file.replace_extension(".info");
|
idx_file.replace_extension(".info");
|
||||||
this->save_info(idx_file.string());
|
this->save_info(idx_file.string());
|
||||||
}
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Preset::reload(Preset const &parent)
|
void Preset::reload(Preset const &parent)
|
||||||
@@ -771,6 +819,7 @@ void Preset::reload(Preset const &parent)
|
|||||||
std::string reason;
|
std::string reason;
|
||||||
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
|
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
|
||||||
try {
|
try {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
|
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
|
||||||
this->config = parent.config;
|
this->config = parent.config;
|
||||||
this->config.apply(std::move(config));
|
this->config.apply(std::move(config));
|
||||||
@@ -1199,6 +1248,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",
|
||||||
@@ -1668,6 +1718,186 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
|
|||||||
return get_preset_canonical_name(parsed.bare, origin);
|
return get_preset_canonical_name(parsed.bare, origin);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PresetCollection::PresetFilesOnDisk PresetCollection::PresetFilesOnDisk::read(const boost::filesystem::path &file)
|
||||||
|
{
|
||||||
|
auto read_if_present = [](const fs::path &path) -> std::optional<std::string> {
|
||||||
|
std::string bytes;
|
||||||
|
try {
|
||||||
|
load_string_file(path, bytes);
|
||||||
|
} catch (const std::exception &) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
};
|
||||||
|
fs::path info_path(file);
|
||||||
|
info_path.replace_extension(".info");
|
||||||
|
return { read_if_present(file), read_if_present(info_path) };
|
||||||
|
}
|
||||||
|
|
||||||
|
PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
|
||||||
|
const boost::filesystem::path &file, const std::string &canonical_name,
|
||||||
|
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
|
||||||
|
const std::string &extruder_id_name, const std::string &extruder_variant_name,
|
||||||
|
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const
|
||||||
|
{
|
||||||
|
UserPresetLoad out;
|
||||||
|
out.preset = Preset(m_type, canonical_name, false);
|
||||||
|
Preset &preset = out.preset;
|
||||||
|
preset.bundle_id = load_origin.bundle_id;
|
||||||
|
preset.file = file.string();
|
||||||
|
// Before either file is parsed, so a save that lands during the parse still shows
|
||||||
|
// up as a difference when commit compares.
|
||||||
|
out.on_disk = PresetFilesOnDisk::read(file);
|
||||||
|
// Load the preset file, apply preset values on top of defaults.
|
||||||
|
try {
|
||||||
|
fs::path idx_path(preset.file);
|
||||||
|
idx_path.replace_extension(".info");
|
||||||
|
if (fs::exists(idx_path)) {
|
||||||
|
out.info_file = idx_path.string();
|
||||||
|
preset.load_info(out.info_file);
|
||||||
|
}
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
//BBS: change to json format
|
||||||
|
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
||||||
|
std::map<std::string, std::string> key_values;
|
||||||
|
std::string reason;
|
||||||
|
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
||||||
|
if (! config_substitutions.empty())
|
||||||
|
out.substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
||||||
|
if (!reason.empty()) {
|
||||||
|
out.discard_file = true;
|
||||||
|
out.errors.push_back((boost::format("parse config %1% failed") % preset.file).str());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
||||||
|
boost::optional<Semver> version = Semver::parse(version_str);
|
||||||
|
if (!version) return out;
|
||||||
|
preset.version = *version;
|
||||||
|
|
||||||
|
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
|
||||||
|
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
|
||||||
|
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
|
||||||
|
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
||||||
|
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
|
||||||
|
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
|
||||||
|
|
||||||
|
//Orca: find and use the inherit config as the base
|
||||||
|
const Preset* inherit_preset = nullptr;
|
||||||
|
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
|
||||||
|
|
||||||
|
// check inherits_config
|
||||||
|
if (inherits_config) {
|
||||||
|
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
|
||||||
|
std::string inherits_value = option_str->value;
|
||||||
|
// Orca: try to find if the parent preset has been renamed
|
||||||
|
inherit_preset = this->find_preset2(inherits_value);
|
||||||
|
Preset::normalize_inherits(config, inherit_preset);
|
||||||
|
}
|
||||||
|
const Preset& default_preset = this->default_preset_for(config);
|
||||||
|
if (inherit_preset) {
|
||||||
|
preset.config = inherit_preset->config;
|
||||||
|
preset.filament_id = inherit_preset->filament_id;
|
||||||
|
extend_default_config_length(config, false, {});
|
||||||
|
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
|
||||||
|
if ((inherits_config2 && !inherits_config2->value.empty())) {
|
||||||
|
out.errors.push_back((boost::format("can not find parent %1% for config %2%!") % inherits_config2->value % preset.file).str());
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
// We support custom root preset now
|
||||||
|
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
|
||||||
|
preset.config = default_preset.config;
|
||||||
|
preset.config.apply(std::move(config));
|
||||||
|
extend_default_config_length(preset.config, true, default_preset.config);
|
||||||
|
}
|
||||||
|
|
||||||
|
Preset::normalize(preset.config);
|
||||||
|
// Report configuration fields, which are misplaced into a wrong group.
|
||||||
|
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
|
||||||
|
if (!incorrect_keys.empty())
|
||||||
|
out.errors.push_back("Error in a preset file: The preset \"" + preset.file +
|
||||||
|
"\" contains the following incorrect keys: " + incorrect_keys + ", which were removed");
|
||||||
|
|
||||||
|
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
|
||||||
|
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
|
||||||
|
if (compatible_printers && compatible_printers->values.empty()) {
|
||||||
|
size_t at_pos = canonical_name.find('@');
|
||||||
|
if (at_pos != std::string::npos && at_pos + 1 < canonical_name.length()) {
|
||||||
|
compatible_printers->values.push_back(canonical_name.substr(at_pos + 1));
|
||||||
|
out.save_compatible_printers = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
preset.loaded = true;
|
||||||
|
out.complete = true;
|
||||||
|
} catch (const std::ifstream::failure &err) {
|
||||||
|
out.discard_file = true;
|
||||||
|
out.errors.push_back((boost::format("The user-config cannot be loaded: %1%. Reason: %2%") % preset.file % err.what()).str());
|
||||||
|
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
||||||
|
} catch (const std::runtime_error &err) {
|
||||||
|
out.discard_file = true;
|
||||||
|
out.errors.push_back((boost::format("Failed loading the user-config file: %1%. Reason: %2%") % preset.file % err.what()).str());
|
||||||
|
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
||||||
|
}
|
||||||
|
out.install = true;
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PresetCollection::commit_user_preset(UserPresetLoad &&loaded, std::deque<Preset> &presets_loaded,
|
||||||
|
PresetsConfigSubstitutions &substitutions,
|
||||||
|
const std::function<void(Preset&)> &preset_loaded_fn,
|
||||||
|
bool read_only)
|
||||||
|
{
|
||||||
|
Preset &preset = loaded.preset;
|
||||||
|
if (! loaded.info_file.empty())
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load info from: " << loaded.info_file << " and base_id: " << preset.base_id;
|
||||||
|
append(substitutions, std::move(loaded.substitutions));
|
||||||
|
for (const std::string &error : loaded.errors) {
|
||||||
|
++m_errors;
|
||||||
|
BOOST_LOG_TRIVIAL(error) << error;
|
||||||
|
}
|
||||||
|
if (loaded.discard_file)
|
||||||
|
remove_preset_files(loaded.preset.file, read_only);
|
||||||
|
if (!loaded.install)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (loaded.complete) {
|
||||||
|
if (loaded.save_compatible_printers) {
|
||||||
|
// The preset loads with the derived printer whether or not its file
|
||||||
|
// can be rewritten.
|
||||||
|
try {
|
||||||
|
if (read_only)
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name << " (not written back)";
|
||||||
|
else if (preset.save(nullptr))
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name;
|
||||||
|
else
|
||||||
|
++m_errors; // save() logged why
|
||||||
|
} catch (const std::runtime_error &err) {
|
||||||
|
++m_errors;
|
||||||
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " can not write compatible_printers back to " << preset.file << ": " << err.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//BBS: add some workaround for previous incorrect settings
|
||||||
|
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
|
||||||
|
preset.setting_id.clear();
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << preset.name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
|
||||||
|
//BBS: add config related logs
|
||||||
|
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
|
||||||
|
// add alias for custom filament preset
|
||||||
|
set_custom_preset_alias(preset);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (preset_loaded_fn != nullptr)
|
||||||
|
preset_loaded_fn(preset);
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
|
||||||
|
presets_loaded.emplace_back(std::move(preset));
|
||||||
|
}
|
||||||
|
|
||||||
// Load all presets found in dir_path.
|
// Load all presets found in dir_path.
|
||||||
// Throws an exception on error.
|
// Throws an exception on error.
|
||||||
void PresetCollection::load_presets(
|
void PresetCollection::load_presets(
|
||||||
@@ -1705,6 +1935,8 @@ void PresetCollection::load_presets(
|
|||||||
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
|
||||||
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
|
||||||
|
|
||||||
|
struct UserPresetFile { fs::path path; std::string canonical_name; };
|
||||||
|
std::vector<UserPresetFile> files;
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
for (auto &dir_entry : boost::filesystem::directory_iterator(dir))
|
||||||
{
|
{
|
||||||
@@ -1720,149 +1952,47 @@ void PresetCollection::load_presets(
|
|||||||
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
|
BOOST_LOG_TRIVIAL(warning) << "Preset already present, not loading: " << canonical_name;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
files.push_back({ dir_entry.path(), std::move(canonical_name) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve_then_commit<CNumericLocalesSetter>(files.size(),
|
||||||
|
[&](size_t i) {
|
||||||
|
return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
|
||||||
|
extruder_id_name, extruder_variant_name, key_set1, key_set2);
|
||||||
|
},
|
||||||
|
[&](size_t i, UserPresetLoad &&loaded) {
|
||||||
|
// Resolve read the files without the lock, so another instance may have saved
|
||||||
|
// over them since. A file that changed is resolved again under the lock before
|
||||||
|
// commit removes or rewrites it, which also keeps its .json and .info from two
|
||||||
|
// different saves apart. Without the lock nothing is checked, so commit leaves
|
||||||
|
// the files alone.
|
||||||
|
const std::string lock_path = user_presets_lock_path(read_only);
|
||||||
|
InstanceLock instance_lock(lock_path);
|
||||||
|
if (instance_lock.locked()) {
|
||||||
|
const PresetFilesOnDisk on_disk = PresetFilesOnDisk::read(files[i].path);
|
||||||
|
boost::system::error_code ec;
|
||||||
|
// The link itself, and a stat error counts as present, so a file that is
|
||||||
|
// there but cannot be read is still counted and removed by commit.
|
||||||
|
if (! on_disk.json && fs::symlink_status(files[i].path, ec).type() == fs::file_not_found)
|
||||||
|
return; // removed by another instance since it was read
|
||||||
|
if (! (on_disk == loaded.on_disk)) {
|
||||||
|
CNumericLocalesSetter locales_setter;
|
||||||
|
loaded = this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
|
||||||
|
extruder_id_name, extruder_variant_name, key_set1, key_set2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool leave_files = read_only || (! lock_path.empty() && ! instance_lock.locked());
|
||||||
|
// Committing can remove an unreadable preset's file, and a filesystem error
|
||||||
|
// there is reported without stopping the rest of the directory.
|
||||||
try {
|
try {
|
||||||
Preset preset(m_type, canonical_name, false);
|
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, leave_files);
|
||||||
preset.bundle_id = resolved_origin.bundle_id;
|
|
||||||
preset.file = dir_entry.path().string();
|
|
||||||
// Load the preset file, apply preset values on top of defaults.
|
|
||||||
try {
|
|
||||||
fs::path idx_path(preset.file);
|
|
||||||
idx_path.replace_extension(".info");
|
|
||||||
if (fs::exists(idx_path)) {
|
|
||||||
preset.load_info(idx_path.string());
|
|
||||||
}
|
|
||||||
DynamicPrintConfig config;
|
|
||||||
//BBS: change to json format
|
|
||||||
//ConfigSubstitutions config_substitutions = config.load_from_ini(preset.file, substitution_rule);
|
|
||||||
std::map<std::string, std::string> key_values;
|
|
||||||
std::string reason;
|
|
||||||
ConfigSubstitutions config_substitutions = config.load_from_json(preset.file, substitution_rule, key_values, reason);
|
|
||||||
if (! config_substitutions.empty())
|
|
||||||
substitutions.push_back({ preset.name, m_type, PresetConfigSubstitutions::Source::UserFile, preset.file, std::move(config_substitutions) });
|
|
||||||
if (!reason.empty()) {
|
|
||||||
fs::path file_path(preset.file);
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("parse config %1% failed")%preset.file;
|
|
||||||
++m_errors;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string version_str = key_values[BBL_JSON_KEY_VERSION];
|
|
||||||
boost::optional<Semver> version = Semver::parse(version_str);
|
|
||||||
if (!version) continue;
|
|
||||||
preset.version = *version;
|
|
||||||
|
|
||||||
if (key_values.find(BBL_JSON_KEY_FILAMENT_ID) != key_values.end())
|
|
||||||
preset.filament_id = key_values[BBL_JSON_KEY_FILAMENT_ID];
|
|
||||||
if (key_values.find(BBL_JSON_KEY_DESCRIPTION) != key_values.end())
|
|
||||||
preset.description = key_values[BBL_JSON_KEY_DESCRIPTION];
|
|
||||||
if (key_values.find(BBL_JSON_KEY_INSTANTIATION) != key_values.end())
|
|
||||||
preset.is_visible = key_values[BBL_JSON_KEY_INSTANTIATION] != "false";
|
|
||||||
|
|
||||||
//Orca: find and use the inherit config as the base
|
|
||||||
Preset* inherit_preset = nullptr;
|
|
||||||
ConfigOption* inherits_config = config.option(BBL_JSON_KEY_INHERITS);
|
|
||||||
|
|
||||||
// check inherits_config
|
|
||||||
if (inherits_config) {
|
|
||||||
ConfigOptionString * option_str = dynamic_cast<ConfigOptionString *> (inherits_config);
|
|
||||||
std::string inherits_value = option_str->value;
|
|
||||||
// Orca: try to find if the parent preset has been renamed
|
|
||||||
inherit_preset = this->find_preset2(inherits_value);
|
|
||||||
Preset::normalize_inherits(config, inherit_preset);
|
|
||||||
} else {
|
|
||||||
;
|
|
||||||
}
|
|
||||||
const Preset& default_preset = this->default_preset_for(config);
|
|
||||||
if (inherit_preset) {
|
|
||||||
preset.config = inherit_preset->config;
|
|
||||||
preset.filament_id = inherit_preset->filament_id;
|
|
||||||
extend_default_config_length(config, false, {});
|
|
||||||
preset.config.update_diff_values_to_child_config(config, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
auto inherits_config2 = dynamic_cast<ConfigOptionString *>(inherits_config);
|
|
||||||
if ((inherits_config2 && !inherits_config2->value.empty())) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("can not find parent %1% for config %2%!")%inherits_config2->value %preset.file;
|
|
||||||
++m_errors;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// We support custom root preset now
|
|
||||||
// Find a default preset for the config. The PrintPresetCollection provides different default preset based on the "printer_technology" field.
|
|
||||||
preset.config = default_preset.config;
|
|
||||||
preset.config.apply(std::move(config));
|
|
||||||
extend_default_config_length(preset.config, true, default_preset.config);
|
|
||||||
}
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " load preset: " << name << " and filament_id: " << preset.filament_id << " and base_id: " << preset.base_id;
|
|
||||||
|
|
||||||
Preset::normalize(preset.config);
|
|
||||||
// Report configuration fields, which are misplaced into a wrong group.
|
|
||||||
std::string incorrect_keys = Preset::remove_invalid_keys(preset.config, default_preset.config);
|
|
||||||
if (!incorrect_keys.empty()) {
|
|
||||||
++m_errors;
|
|
||||||
BOOST_LOG_TRIVIAL(error)
|
|
||||||
<< "Error in a preset file: The preset \"" << preset.file
|
|
||||||
<< "\" contains the following incorrect keys: " << incorrect_keys << ", which were removed";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preset.type == Preset::TYPE_FILAMENT && preset.is_user() && preset.inherits().empty()) {
|
|
||||||
auto compatible_printers = dynamic_cast<ConfigOptionStrings *>(preset.config.option("compatible_printers", true));
|
|
||||||
if (compatible_printers && compatible_printers->values.empty()) {
|
|
||||||
size_t at_pos = name.find('@');
|
|
||||||
if (at_pos != std::string::npos && at_pos + 1 < name.length()) {
|
|
||||||
compatible_printers->values.push_back(name.substr(at_pos + 1));
|
|
||||||
if (!read_only)
|
|
||||||
preset.save(nullptr);
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
preset.loaded = true;
|
|
||||||
//BBS: add some workaround for previous incorrect settings
|
|
||||||
if ((!preset.setting_id.empty())&&(preset.setting_id == preset.base_id))
|
|
||||||
preset.setting_id.clear();
|
|
||||||
//BBS: add config related logs
|
|
||||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(", preset type %1%, name %2%, path %3%, is_system %4%, is_default %5% is_visible %6%")%Preset::get_type_string(m_type) %preset.name %preset.file %preset.is_system %preset.is_default %preset.is_visible;
|
|
||||||
// add alias for custom filament preset
|
|
||||||
set_custom_preset_alias(preset);
|
|
||||||
} catch (const std::ifstream::failure &err) {
|
|
||||||
++m_errors;
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("The user-config cannot be loaded: %1%. Reason: %2%")%preset.file %err.what();
|
|
||||||
fs::path file_path(preset.file);
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
//throw Slic3r::RuntimeError(std::string("The selected preset cannot be loaded: ") + preset.file + "\n\tReason: " + err.what());
|
|
||||||
} catch (const std::runtime_error &err) {
|
|
||||||
++m_errors;
|
|
||||||
BOOST_LOG_TRIVIAL(error) << boost::format("Failed loading the user-config file: %1%. Reason: %2%")%preset.file %err.what();
|
|
||||||
//throw Slic3r::RuntimeError(std::string("Failed loading the preset file: ") + preset.file + "\n\tReason: " + err.what());
|
|
||||||
fs::path file_path(preset.file);
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
file_path.replace_extension(".info");
|
|
||||||
if (!read_only && fs::exists(file_path))
|
|
||||||
fs::remove(file_path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preset_loaded_fn != nullptr)
|
|
||||||
preset_loaded_fn(preset);
|
|
||||||
|
|
||||||
presets_loaded.emplace_back(preset);
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << __LINE__ << " load config successful and preset name is:" << preset.name;
|
|
||||||
} catch (const std::runtime_error &err) {
|
} catch (const std::runtime_error &err) {
|
||||||
errors_cummulative += err.what();
|
errors_cummulative += err.what();
|
||||||
errors_cummulative += "\n";
|
errors_cummulative += "\n";
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
}
|
|
||||||
if (presets_loaded.size() > 0)
|
if (presets_loaded.size() > 0)
|
||||||
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
|
m_presets.insert(m_presets.end(), std::make_move_iterator(presets_loaded.begin()), std::make_move_iterator(presets_loaded.end()));
|
||||||
sort_presets();
|
sort_presets();
|
||||||
@@ -2166,7 +2296,10 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
|
|||||||
preset->setting_id = setting_id;
|
preset->setting_id = setting_id;
|
||||||
if (update_time > 0)
|
if (update_time > 0)
|
||||||
preset->updated_time = update_time;
|
preset->updated_time = update_time;
|
||||||
preset->sync_info == "update" ? preset->save(nullptr) : preset->save_info();
|
if (preset->sync_info == "update")
|
||||||
|
preset->save(nullptr);
|
||||||
|
else
|
||||||
|
preset->save_info();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2826,6 +2959,18 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
|
|||||||
return std::make_pair(&preset, false);
|
return std::make_pair(&preset, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Preset& PresetCollection::append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config)
|
||||||
|
{
|
||||||
|
lock();
|
||||||
|
Preset &preset = m_presets.emplace_back(m_type, name, false);
|
||||||
|
preset.file = std::move(path);
|
||||||
|
preset.config = std::move(config);
|
||||||
|
preset.loaded = true;
|
||||||
|
preset.is_dirty = false;
|
||||||
|
unlock();
|
||||||
|
return preset;
|
||||||
|
}
|
||||||
|
|
||||||
Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
|
Preset& PresetCollection::load_preset(const std::string &path, const std::string &name, DynamicPrintConfig &&config, bool select, Semver file_version)
|
||||||
{
|
{
|
||||||
lock();
|
lock();
|
||||||
@@ -3845,28 +3990,53 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
std::vector<std::vector<std::string>> PresetCollection::merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors)
|
||||||
std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors)
|
|
||||||
{
|
{
|
||||||
std::vector<std::string> duplicates;
|
auto less = [this](const Preset &a, const Preset &b) {
|
||||||
for (Preset &preset : other.m_presets) {
|
return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b;
|
||||||
if (preset.is_default || preset.is_external)
|
};
|
||||||
continue;
|
struct Incoming { Preset *preset; size_t source; };
|
||||||
Preset key(m_type, preset.name);
|
auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); };
|
||||||
auto it = (m_type == Preset::TYPE_FILAMENT)
|
// Each of `others` is sorted, so its presets form one sorted run.
|
||||||
? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less)
|
std::vector<Incoming> incoming;
|
||||||
: std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key);
|
std::vector<size_t> run_ends { 0 };
|
||||||
if (it == m_presets.end() || it->name != preset.name) {
|
for (size_t source = 0; source < others.size(); ++ source) {
|
||||||
|
for (Preset &preset : others[source]->m_presets)
|
||||||
|
if (! preset.is_default && ! preset.is_external)
|
||||||
|
incoming.push_back({ &preset, source });
|
||||||
|
assert(std::is_sorted(incoming.begin() + run_ends.back(), incoming.end(), incoming_less));
|
||||||
|
run_ends.push_back(incoming.size());
|
||||||
|
}
|
||||||
|
// Merged pairwise and stably, so equal names stay in the order of `others`.
|
||||||
|
const size_t runs = others.size();
|
||||||
|
for (size_t width = 1; width < runs; width *= 2)
|
||||||
|
for (size_t i = 0; i + width < runs; i += 2 * width)
|
||||||
|
std::inplace_merge(incoming.begin() + run_ends[i], incoming.begin() + run_ends[i + width],
|
||||||
|
incoming.begin() + run_ends[std::min(i + 2 * width, runs)], incoming_less);
|
||||||
|
|
||||||
|
std::vector<std::vector<std::string>> duplicates(others.size());
|
||||||
|
std::deque<Preset> merged;
|
||||||
|
auto own = m_presets.begin() + m_num_default_presets;
|
||||||
|
std::move(m_presets.begin(), own, std::back_inserter(merged));
|
||||||
|
// On equal names this collection's preset is kept, else the earliest of `others`,
|
||||||
|
// and each repeat is listed under the collection it came from.
|
||||||
|
for (auto next = incoming.begin(); own != m_presets.end() || next != incoming.end();) {
|
||||||
|
if (next == incoming.end() || (own != m_presets.end() && ! less(*next->preset, *own)))
|
||||||
|
merged.emplace_back(std::move(*own ++));
|
||||||
|
else {
|
||||||
|
Preset &preset = *(next ++)->preset;
|
||||||
if (preset.vendor != nullptr) {
|
if (preset.vendor != nullptr) {
|
||||||
// Re-assign a pointer to the vendor structure in the new PresetBundle.
|
// Re-assign a pointer to the vendor structure in the new PresetBundle.
|
||||||
auto it = new_vendors.find(preset.vendor->id);
|
auto it = new_vendors.find(preset.vendor->id);
|
||||||
assert(it != new_vendors.end());
|
assert(it != new_vendors.end());
|
||||||
preset.vendor = &it->second;
|
preset.vendor = &it->second;
|
||||||
}
|
}
|
||||||
m_presets.emplace(it, std::move(preset));
|
merged.emplace_back(std::move(preset));
|
||||||
} else
|
|
||||||
duplicates.emplace_back(std::move(preset.name));
|
|
||||||
}
|
}
|
||||||
|
for (; next != incoming.end() && next->preset->name == merged.back().name; ++ next)
|
||||||
|
duplicates[next->source].emplace_back(next->preset->name);
|
||||||
|
}
|
||||||
|
m_presets = std::move(merged);
|
||||||
return duplicates;
|
return duplicates;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4211,8 +4381,15 @@ void PhysicalPrinter::update_preset_names_in_config()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PhysicalPrinter::save(DynamicPrintConfig* /* parent_config */)
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION));
|
||||||
|
}
|
||||||
|
|
||||||
void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to)
|
void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to)
|
||||||
{
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
// rename the file
|
// rename the file
|
||||||
boost::nowide::rename(file_name_from.data(), file_name_to.data());
|
boost::nowide::rename(file_name_from.data(), file_name_to.data());
|
||||||
this->file = file_name_to;
|
this->file = file_name_to;
|
||||||
@@ -4344,6 +4521,7 @@ void PhysicalPrinterCollection::load_printers(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
PhysicalPrinter printer(name, this->default_config());
|
PhysicalPrinter printer(name, this->default_config());
|
||||||
printer.file = dir_entry.path().string();
|
printer.file = dir_entry.path().string();
|
||||||
// Load the preset file, apply preset values on top of defaults.
|
// Load the preset file, apply preset values on top of defaults.
|
||||||
@@ -4536,7 +4714,10 @@ bool PhysicalPrinterCollection::delete_printer(const std::string& name)
|
|||||||
|
|
||||||
const PhysicalPrinter& printer = *it;
|
const PhysicalPrinter& printer = *it;
|
||||||
// Erase the preset file.
|
// Erase the preset file.
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::nowide::remove(printer.file.c_str());
|
boost::nowide::remove(printer.file.c_str());
|
||||||
|
}
|
||||||
m_printers.erase(it);
|
m_printers.erase(it);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -4548,7 +4729,10 @@ bool PhysicalPrinterCollection::delete_selected_printer()
|
|||||||
const PhysicalPrinter& printer = this->get_selected_printer();
|
const PhysicalPrinter& printer = this->get_selected_printer();
|
||||||
|
|
||||||
// Erase the preset file.
|
// Erase the preset file.
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::nowide::remove(printer.file.c_str());
|
boost::nowide::remove(printer.file.c_str());
|
||||||
|
}
|
||||||
// Remove the preset from the list.
|
// Remove the preset from the list.
|
||||||
m_printers.erase(m_printers.begin() + m_idx_selected);
|
m_printers.erase(m_printers.begin() + m_idx_selected);
|
||||||
// unselect all printers
|
// unselect all printers
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
|
#include <optional>
|
||||||
#include <boost/algorithm/string/predicate.hpp>
|
#include <boost/algorithm/string/predicate.hpp>
|
||||||
#include <boost/filesystem/path.hpp>
|
#include <boost/filesystem/path.hpp>
|
||||||
#include <boost/property_tree/ptree_fwd.hpp>
|
#include <boost/property_tree/ptree_fwd.hpp>
|
||||||
@@ -330,7 +331,8 @@ public:
|
|||||||
|
|
||||||
//BBS: add logic for only difference save
|
//BBS: add logic for only difference save
|
||||||
//if parent_config is null, save all keys, otherwise, only save difference
|
//if parent_config is null, save all keys, otherwise, only save difference
|
||||||
void save(DynamicPrintConfig* parent_config);
|
// Returns false when the preset file could not be written.
|
||||||
|
bool save(DynamicPrintConfig* parent_config);
|
||||||
void reload(Preset const & parent);
|
void reload(Preset const & parent);
|
||||||
|
|
||||||
// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
|
// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
|
||||||
@@ -445,7 +447,10 @@ public:
|
|||||||
static std::string remove_suffix_modified(const std::string& name);
|
static std::string remove_suffix_modified(const std::string& name);
|
||||||
static void normalize(DynamicPrintConfig &config);
|
static void normalize(DynamicPrintConfig &config);
|
||||||
// Report configuration fields, which are misplaced into a wrong group, remove them from the config.
|
// Report configuration fields, which are misplaced into a wrong group, remove them from the config.
|
||||||
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config);
|
// `added`, when given, is the diff applied over a copy of default_config, and only
|
||||||
|
// its keys are checked, since no other key can be missing from default_config.
|
||||||
|
static std::string remove_invalid_keys(DynamicPrintConfig &config, const DynamicPrintConfig &default_config,
|
||||||
|
const DynamicPrintConfig *added = nullptr);
|
||||||
|
|
||||||
// BBS: move constructor to public
|
// BBS: move constructor to public
|
||||||
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
Preset(Type type, const std::string &name, bool is_default = false) : type(type), is_default(is_default), name(name) {}
|
||||||
@@ -485,6 +490,12 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
|
|||||||
// Tail segment of a canonical name — what's written to the bundle's .json filename and JSON "name" field.
|
// Tail segment of a canonical name — what's written to the bundle's .json filename and JSON "name" field.
|
||||||
std::string get_preset_bare_name(const std::string &canonical_name);
|
std::string get_preset_bare_name(const std::string &canonical_name);
|
||||||
|
|
||||||
|
// Lock file guarding every user preset file under data_dir() against other
|
||||||
|
// running instances and the preset sync thread. Empty without a data dir, and
|
||||||
|
// for a read-only load (the CLI), which never rewrites or deletes and may run
|
||||||
|
// many jobs on one data dir.
|
||||||
|
std::string user_presets_lock_path(bool read_only = false);
|
||||||
|
|
||||||
// Resolve an origin from a directory path when the caller passes Kind::Auto.
|
// Resolve an origin from a directory path when the caller passes Kind::Auto.
|
||||||
PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin = PresetOrigin());
|
PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin = PresetOrigin());
|
||||||
|
|
||||||
@@ -801,6 +812,8 @@ public:
|
|||||||
// Return number of presets including the "- default -" preset.
|
// Return number of presets including the "- default -" preset.
|
||||||
size_t size() const { return m_presets.size(); }
|
size_t size() const { return m_presets.size(); }
|
||||||
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
|
bool has_defaults_only() const { return m_presets.size() <= m_num_default_presets; }
|
||||||
|
// How many presets this collection refused or repaired while loading.
|
||||||
|
int error_count() const { return m_errors; }
|
||||||
|
|
||||||
// For Print / Filament presets, disable those, which are not compatible with the printer.
|
// For Print / Filament presets, disable those, which are not compatible with the printer.
|
||||||
template<typename PreferedCondition>
|
template<typename PreferedCondition>
|
||||||
@@ -874,8 +887,10 @@ protected:
|
|||||||
// This is a temporary state, which shall be fixed immediately by the following step.
|
// This is a temporary state, which shall be fixed immediately by the following step.
|
||||||
bool select_preset_by_name_strict(const std::string &name);
|
bool select_preset_by_name_strict(const std::string &name);
|
||||||
|
|
||||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
// Move the presets of `others` into this collection in one pass. A name this
|
||||||
std::vector<std::string> merge_presets(PresetCollection &&other, const VendorMap &new_vendors);
|
// collection or an earlier one of `others` already has is left out, and reported
|
||||||
|
// in the list of the collection that repeats it.
|
||||||
|
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors);
|
||||||
|
|
||||||
// Update m_map_alias_to_profile_name from loaded system profiles.
|
// Update m_map_alias_to_profile_name from loaded system profiles.
|
||||||
void update_map_alias_to_profile_name();
|
void update_map_alias_to_profile_name();
|
||||||
@@ -890,6 +905,56 @@ protected:
|
|||||||
void set_custom_preset_alias(Preset &preset);
|
void set_custom_preset_alias(Preset &preset);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// A preset file and its .info as read from disk, std::nullopt for one that is missing.
|
||||||
|
struct PresetFilesOnDisk
|
||||||
|
{
|
||||||
|
std::optional<std::string> json;
|
||||||
|
std::optional<std::string> info;
|
||||||
|
|
||||||
|
static PresetFilesOnDisk read(const boost::filesystem::path &file);
|
||||||
|
bool operator==(const PresetFilesOnDisk &rhs) const { return json == rhs.json && info == rhs.info; }
|
||||||
|
};
|
||||||
|
|
||||||
|
// One preset file read and flattened against the presets already in this
|
||||||
|
// collection, before anything the collection shares has been touched.
|
||||||
|
struct UserPresetLoad
|
||||||
|
{
|
||||||
|
Preset preset;
|
||||||
|
// Joins the collection. A file that threw partway still joins it, without
|
||||||
|
// the steps that did not run.
|
||||||
|
bool install { false };
|
||||||
|
// The whole of the load ran, so the preset is ready to be aliased.
|
||||||
|
bool complete { false };
|
||||||
|
// A filament preset that named no compatible printer and was given one from
|
||||||
|
// its name, which commit writes back to its file.
|
||||||
|
bool save_compatible_printers { false };
|
||||||
|
// Unreadable, so commit removes it and its .info file.
|
||||||
|
bool discard_file { false };
|
||||||
|
// The .info file read beside the preset, which commit logs.
|
||||||
|
std::string info_file;
|
||||||
|
// Both files as they were before the preset was read from them. Resolve runs
|
||||||
|
// without the instance lock, so commit compares this with the disk under it.
|
||||||
|
PresetFilesOnDisk on_disk;
|
||||||
|
// Counted and logged by commit, in the order the directory listed the files.
|
||||||
|
std::vector<std::string> errors;
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read and flatten one preset file. It reads only, and resolves against the presets
|
||||||
|
// loaded before this pass, never another file of the same pass, so the files of a
|
||||||
|
// pass are independent of each other.
|
||||||
|
UserPresetLoad resolve_user_preset(const boost::filesystem::path &file, const std::string &canonical_name,
|
||||||
|
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
|
||||||
|
const std::string &extruder_id_name, const std::string &extruder_variant_name,
|
||||||
|
std::set<std::string> *key_set1, std::set<std::string> *key_set2) const;
|
||||||
|
|
||||||
|
// Install one resolved preset. The collection, its alias maps, the error count
|
||||||
|
// and the preset files on disk are touched here and only here.
|
||||||
|
void commit_user_preset(UserPresetLoad &&loaded, std::deque<Preset> &presets_loaded,
|
||||||
|
PresetsConfigSubstitutions &substitutions,
|
||||||
|
const std::function<void(Preset&)> &preset_loaded_fn,
|
||||||
|
bool read_only);
|
||||||
|
|
||||||
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const;
|
std::string canonical_preset_name(const std::string &name, const PresetOrigin &load_origin = PresetOrigin()) const;
|
||||||
|
|
||||||
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
|
||||||
@@ -901,6 +966,10 @@ private:
|
|||||||
return a.name < b.name;
|
return a.name < b.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Append a preset without keeping the collection sorted, for a caller installing
|
||||||
|
// many at once; find_preset() is unusable until sort_presets() runs.
|
||||||
|
Preset& append_preset(std::string &&path, const std::string &name, DynamicPrintConfig &&config);
|
||||||
|
|
||||||
// Sort presets: filament presets use generic-first ordering, others sort alphabetically.
|
// Sort presets: filament presets use generic-first ordering, others sort alphabetically.
|
||||||
void sort_presets() {
|
void sort_presets() {
|
||||||
if (m_type == Preset::TYPE_FILAMENT)
|
if (m_type == Preset::TYPE_FILAMENT)
|
||||||
@@ -1054,7 +1123,7 @@ public:
|
|||||||
|
|
||||||
//BBS: change to json format
|
//BBS: change to json format
|
||||||
//void save() { this->config.save(this->file); }
|
//void save() { this->config.save(this->file); }
|
||||||
void save(DynamicPrintConfig* parent_config) { this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION)); }
|
void save(DynamicPrintConfig* parent_config);
|
||||||
void save(const std::string& file_name_from, const std::string& file_name_to);
|
void save(const std::string& file_name_from, const std::string& file_name_to);
|
||||||
|
|
||||||
void update_from_preset(const Preset& preset);
|
void update_from_preset(const Preset& preset);
|
||||||
|
|||||||
+584
-398
File diff suppressed because it is too large
Load Diff
+172
-27
@@ -15,6 +15,7 @@
|
|||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <array>
|
#include <array>
|
||||||
|
#include <atomic>
|
||||||
#include <boost/filesystem/path.hpp>
|
#include <boost/filesystem/path.hpp>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
|
|
||||||
@@ -622,36 +623,180 @@ public:
|
|||||||
// default_filament_profile must resolve to a system filament.
|
// default_filament_profile must resolve to a system filament.
|
||||||
bool check_printer_default_materials() const;
|
bool check_printer_default_materials() const;
|
||||||
|
|
||||||
// Merge one vendor's presets with the other vendor's presets, report duplicates.
|
// One vendor to load, and the directory it is installed in.
|
||||||
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
|
struct VendorSource
|
||||||
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
|
{
|
||||||
std::vector<std::string> merge_presets(PresetBundle &&other);
|
std::string name;
|
||||||
|
boost::filesystem::path dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Load `vendors` into this bundle, the Orca filament library directly and every
|
||||||
|
// other vendor in parallel into a bundle of its own that inherits from it, merged
|
||||||
|
// in the order given. A vendor that cannot be loaded has its error added to the
|
||||||
|
// returned text, or thrown in validation mode, and its name to `failed`; it is
|
||||||
|
// left out, except for the library, which keeps what it installed before the
|
||||||
|
// failure. Once `cancel` is set, no further vendor starts loading.
|
||||||
|
std::pair<PresetsConfigSubstitutions, std::string> load_vendors(const std::vector<VendorSource>& vendors,
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache,
|
||||||
|
const std::atomic<bool>* cancel = nullptr, std::vector<std::string>* failed = nullptr);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
// Load one vendor from the preset cache installed in `dir`, judged against
|
// Move the presets and vendor profiles of `others` into this bundle, in one pass
|
||||||
// the vendor profile there. False, with this bundle left clean, when there
|
// over each collection. A preset whose name this bundle or an earlier one of
|
||||||
// is no usable cache and the vendor has to be parsed. This is how
|
// `others` already has is left out and listed under the bundle that repeats it.
|
||||||
// load_vendor_configs_from_json reads a cache.
|
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetBundle*> &others);
|
||||||
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
|
|
||||||
|
|
||||||
// Load one source-form preset entry into this bundle: resolve `inherits`
|
// What parsing one entry's JSON sub-file reported. Its errors and warnings are
|
||||||
// and `include`, flatten, validate and register the preset. Returns the
|
// logged when the entry installs, so they come out in listing order with the
|
||||||
// reason loading failed, empty on success. See the definition for the
|
// entry's install errors, as parsing and installing one entry at a time leaves them.
|
||||||
// sharing contract between the JSON parse and the cache load.
|
struct EntryParse
|
||||||
// retain_configs / retain_includes, when non-null, name the only presets
|
{
|
||||||
// registered into config_maps / include_maps (a config copy each). The
|
ConfigSubstitutions substitutions;
|
||||||
// cache load passes the names its entries inherit / include — the only
|
// Counted in the bundle's error count.
|
||||||
// ones ever looked up again; the JSON parse retains all, not knowing what
|
std::vector<std::string> errors;
|
||||||
// later subfiles name.
|
std::vector<std::string> warnings;
|
||||||
std::string load_vendor_preset(const CachedPreset& entry,
|
};
|
||||||
const std::string& path, const std::string& vendor_name,
|
|
||||||
const PresetBundle* base_bundle,
|
// An EntryParse for each entry of the VendorCacheData list of the same name.
|
||||||
LoadConfigBundleAttributes flags,
|
struct VendorParse
|
||||||
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
|
{
|
||||||
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
|
std::vector<EntryParse> process_entries;
|
||||||
std::map<std::string, std::string>& filament_id_maps,
|
std::vector<EntryParse> filament_entries;
|
||||||
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
|
std::vector<EntryParse> machine_entries;
|
||||||
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
|
};
|
||||||
|
|
||||||
|
// One vendor read from its cache or its JSONs by read_vendor, for
|
||||||
|
// install_vendor_read to install.
|
||||||
|
struct VendorRead
|
||||||
|
{
|
||||||
|
std::string dir;
|
||||||
|
std::string vendor_name;
|
||||||
|
LoadConfigBundleAttributes flags;
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule;
|
||||||
|
// The errors this bundle had counted before the read, which the cache stamp leaves out.
|
||||||
|
int errors_at_entry { 0 };
|
||||||
|
// A whole-vendor load, which can read a cache and write one.
|
||||||
|
bool cacheable { false };
|
||||||
|
// Read from the cache at cache_path, which install can still reject.
|
||||||
|
bool from_cache { false };
|
||||||
|
std::string cache_path;
|
||||||
|
// Only the vendor profile was asked for.
|
||||||
|
bool vendor_only { false };
|
||||||
|
VendorCacheData data;
|
||||||
|
// What each entry's JSON parse reported, and whether and with which version
|
||||||
|
// the cache is written once the entries install.
|
||||||
|
VendorParse parsed;
|
||||||
|
bool will_cache { false };
|
||||||
|
std::string version;
|
||||||
|
// The sub-file the parse stopped at, its kind, why, and the errors it reported.
|
||||||
|
std::string reason;
|
||||||
|
std::string failed_subfile;
|
||||||
|
const char* failed_kind { nullptr };
|
||||||
|
std::vector<std::string> failed_errors;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Read a vendor into this bundle's vendor profiles and `data`, from its cache
|
||||||
|
// when one covers it, else from its JSONs; nothing is installed. Throws
|
||||||
|
// ConfigurationError when the vendor's own JSON cannot be parsed.
|
||||||
|
VendorRead read_vendor(const std::string& dir, const std::string& vendor_name, LoadConfigBundleAttributes flags,
|
||||||
|
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache);
|
||||||
|
|
||||||
|
// Parse the vendor's JSONs into `read`, up to the first sub-file that fails.
|
||||||
|
void parse_vendor_json(VendorRead& read);
|
||||||
|
|
||||||
|
// Install what read_vendor read, against base_bundle's filament library. A cache
|
||||||
|
// that cannot be installed is replaced by a parse of the JSONs. Throws
|
||||||
|
// ConfigurationError at the first entry that cannot be installed, or after
|
||||||
|
// installing the entries before a sub-file that could not be parsed.
|
||||||
|
std::pair<PresetsConfigSubstitutions, size_t> install_vendor_read(VendorRead&& read, const PresetBundle* base_bundle);
|
||||||
|
|
||||||
|
// Install a cache's entries. False, with this bundle left clean, when one of them
|
||||||
|
// cannot be installed.
|
||||||
|
bool install_vendor_cache(const std::string& cache_path, const std::string& vendor_name, VendorCacheData&& data,
|
||||||
|
const PresetBundle* base_bundle);
|
||||||
|
|
||||||
|
// Log and count errors reported by a resolve or a parse.
|
||||||
|
void log_errors(const std::vector<std::string>& errors);
|
||||||
|
|
||||||
|
// The state of installing one collection of one vendor, which
|
||||||
|
// resolve_vendor_preset reads through a const reference and only
|
||||||
|
// commit_vendor_preset writes.
|
||||||
|
struct VendorInstall
|
||||||
|
{
|
||||||
|
// The directory holding <vendor_name>/, whose sub-paths the entries name.
|
||||||
|
std::string path;
|
||||||
|
std::string vendor_name;
|
||||||
|
const VendorProfile* vendor_profile;
|
||||||
|
const PresetBundle* base_bundle;
|
||||||
|
LoadConfigBundleAttributes flags;
|
||||||
|
PresetCollection* presets;
|
||||||
|
// The Orca filament library, which keeps every config for other vendors to
|
||||||
|
// resolve against.
|
||||||
|
bool is_from_lib;
|
||||||
|
PresetsConfigSubstitutions* substitutions;
|
||||||
|
// The names some entry inherits / includes, the only ones whose configs /
|
||||||
|
// include diffs are looked up again.
|
||||||
|
std::set<std::string> inherited;
|
||||||
|
std::set<std::string> included;
|
||||||
|
std::map<std::string, DynamicPrintConfig> config_maps;
|
||||||
|
std::map<std::string, DynamicPrintConfig> include_maps;
|
||||||
|
std::map<std::string, std::string> filament_id_maps;
|
||||||
|
std::unordered_set<std::string> installed_names;
|
||||||
|
size_t count { 0 };
|
||||||
|
};
|
||||||
|
|
||||||
|
// Install a vendor's source-form entries, parsed from its JSON or read from its
|
||||||
|
// cache: processes, then filaments, then printers. Both loads go through here,
|
||||||
|
// so a cache-loaded bundle cannot come out different from a JSON-loaded one.
|
||||||
|
// `parsed` is given for entries parsed just now. `complete` says the entries
|
||||||
|
// are the vendor's whole lists; only then are the filament library's configs
|
||||||
|
// and filament ids left in m_config_maps and m_filament_id_maps. Returns the
|
||||||
|
// number of presets installed, and throws ConfigurationError at the first
|
||||||
|
// entry that cannot be installed.
|
||||||
|
size_t install_vendor(const std::string& path, const std::string& vendor_name, const PresetBundle* base_bundle,
|
||||||
|
LoadConfigBundleAttributes flags, const VendorCacheData& entries,
|
||||||
|
VendorParse* parsed, bool complete, PresetsConfigSubstitutions& substitutions);
|
||||||
|
|
||||||
|
// Install one collection's entries in the order they are listed.
|
||||||
|
void install_vendor_entries(VendorInstall& install, const std::vector<CachedPreset>& entries,
|
||||||
|
std::vector<EntryParse>* parsed);
|
||||||
|
|
||||||
|
// One entry flattened against the preset it inherits, before anything this
|
||||||
|
// bundle shares has been touched.
|
||||||
|
struct PresetInstall
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
std::string file_path;
|
||||||
|
// Empty when the preset is its own alias.
|
||||||
|
std::string alias;
|
||||||
|
std::string filament_id;
|
||||||
|
std::vector<std::string> renamed_from;
|
||||||
|
// Reported by commit, so resolving entries together leaves the log and
|
||||||
|
// the error count as one entry at a time produces them.
|
||||||
|
std::vector<std::string> errors;
|
||||||
|
// What a base states for the presets that include it, when it is retained.
|
||||||
|
std::optional<DynamicPrintConfig> included;
|
||||||
|
// The config kept for the entries that inherit this one, or for other
|
||||||
|
// vendors when this is the filament library.
|
||||||
|
std::optional<DynamicPrintConfig> retained;
|
||||||
|
// Not instantiated, so it contributes a config and no preset.
|
||||||
|
bool config_only { false };
|
||||||
|
// Non-empty when the entry is rejected, and says why.
|
||||||
|
std::string reason;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Flatten one entry against the config it inherits, from this collection's
|
||||||
|
// config_maps or base_bundle's filament library, with the include diffs it
|
||||||
|
// names layered in. It looks up nothing but the names the entry inherits and
|
||||||
|
// includes, and writes nothing.
|
||||||
|
PresetInstall resolve_vendor_preset(const CachedPreset& entry, const VendorInstall& install) const;
|
||||||
|
|
||||||
|
// Install a resolved entry. The collections, the maps in `install` and the
|
||||||
|
// error count are touched here and only here, one entry at a time.
|
||||||
|
// Presets are appended, so a repeated name is caught with `installed_names`,
|
||||||
|
// and the collection is sorted once every entry is in.
|
||||||
|
std::string commit_vendor_preset(const CachedPreset& entry, PresetInstall&& resolved,
|
||||||
|
ConfigSubstitutions&& substitutions, VendorInstall& install);
|
||||||
|
|
||||||
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
// Clear every collection's m_printer_hold_alias, which reset() leaves alone.
|
||||||
void clear_printer_hold_aliases();
|
void clear_printer_hold_aliases();
|
||||||
|
|||||||
@@ -400,46 +400,24 @@ bool write_cache_blob(const std::string& path, const std::string& blob)
|
|||||||
{
|
{
|
||||||
boost::crc_32_type crc;
|
boost::crc_32_type crc;
|
||||||
crc.process_bytes(blob.data(), blob.size());
|
crc.process_bytes(blob.data(), blob.size());
|
||||||
// Written beside the target and moved into place, as AppConfig::save does:
|
// Written beside the target and moved into place: a cache is truncated and
|
||||||
// a cache is truncated and rewritten in full, so a write that dies partway
|
// rewritten in full, so a write that dies partway would otherwise leave a
|
||||||
// would otherwise leave a header claiming more body than the file holds.
|
// header claiming more body than the file holds.
|
||||||
// The PID suffix also keeps two instances writing the same vendor from
|
|
||||||
// interleaving.
|
|
||||||
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
|
|
||||||
try {
|
try {
|
||||||
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
|
||||||
{
|
|
||||||
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
|
|
||||||
if (!ofs.is_open()) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
CacheFileHeader fhdr;
|
CacheFileHeader fhdr;
|
||||||
fhdr.magic = CACHE_MAGIC;
|
fhdr.magic = CACHE_MAGIC;
|
||||||
fhdr.version = CACHE_VERSION;
|
fhdr.version = CACHE_VERSION;
|
||||||
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
fhdr.data_size = static_cast<uint64_t>(blob.size());
|
||||||
fhdr.crc32 = crc.checksum();
|
fhdr.crc32 = crc.checksum();
|
||||||
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
const std::string_view header(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
|
||||||
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
|
if (const std::error_code ec = write_file_atomically(path, { header, std::string_view(blob) }, /*binary=*/true)) {
|
||||||
ofs.close(); // flush; close() raises failbit on error
|
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << ec.message();
|
||||||
if (! ofs.good()) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")";
|
|
||||||
boost::system::error_code ec;
|
|
||||||
boost::filesystem::remove(tmp_path, ec);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (const std::error_code ec = rename_file(tmp_path, path)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message();
|
|
||||||
boost::system::error_code rm;
|
|
||||||
boost::filesystem::remove(tmp_path, rm);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
|
||||||
boost::system::error_code ec;
|
|
||||||
boost::filesystem::remove(tmp_path, ec);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ void skip_config(cereal::BinaryInputArchive& ar, const CacheDictionary& dict);
|
|||||||
|
|
||||||
// One preset as its JSON subfile states it: the config diff, the names of the
|
// One preset as its JSON subfile states it: the config diff, the names of the
|
||||||
// preset it inherits and the presets it includes, and the parse metadata —
|
// preset it inherits and the presets it includes, and the parse metadata —
|
||||||
// everything the parse phase of load_vendor_configs_from_json extracts and
|
// everything PresetBundle::parse_vendor_json extracts and
|
||||||
// nothing it derives. Inheritance and includes are resolved when the entry is
|
// nothing it derives. Inheritance and includes are resolved when the entry is
|
||||||
// installed, against whatever filament library is loaded then, so a cache
|
// installed, against whatever filament library is loaded then, so a cache
|
||||||
// carries no other vendor's values and no other vendor's update can make it
|
// carries no other vendor's values and no other vendor's update can make it
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.");
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -8,6 +8,8 @@
|
|||||||
#include <functional>
|
#include <functional>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <system_error>
|
#include <system_error>
|
||||||
|
#include <initializer_list>
|
||||||
|
#include <string_view>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include <boost/system/error_code.hpp>
|
#include <boost/system/error_code.hpp>
|
||||||
@@ -224,6 +226,21 @@ extern std::vector<std::string> split_string(const std::string &str, char delimi
|
|||||||
// On Windows, the file explorer (or anti-virus or whatever else) often locks the file
|
// On Windows, the file explorer (or anti-virus or whatever else) often locks the file
|
||||||
// for a short while, so the file may not be movable. Retry while we see recoverable errors.
|
// for a short while, so the file may not be movable. Retry while we see recoverable errors.
|
||||||
extern std::error_code rename_file(const std::string &from, const std::string &to);
|
extern std::error_code rename_file(const std::string &from, const std::string &to);
|
||||||
|
// Write `chunks`, in order, to `path` through a temporary file beside it that is
|
||||||
|
// then renamed over the target, so a concurrent reader sees the old or the new
|
||||||
|
// file, never a partial one. The temporary is removed on failure and an existing
|
||||||
|
// target keeps its permissions. Text mode unless `binary`, so Windows writes CRLF
|
||||||
|
// as the streams this replaces did. A target that is not a regular file (a
|
||||||
|
// device or pipe) is written in place, since replacing it would change what it
|
||||||
|
// is, and so is an existing target beside which no temporary can be created or
|
||||||
|
// whose replace the filesystem refuses; a symlink is followed and the file it
|
||||||
|
// names is replaced. On Windows a reader holding the
|
||||||
|
// target open without sharing its deletion, which the C runtime does not, makes
|
||||||
|
// the replace fall back to the in-place write too, so an unlocked reader there
|
||||||
|
// can still see a partial file.
|
||||||
|
extern std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary = false);
|
||||||
|
inline std::error_code write_file_atomically(const std::string &path, const std::string &content, bool binary = false)
|
||||||
|
{ return write_file_atomically(path, { std::string_view(content) }, binary); }
|
||||||
|
|
||||||
enum CopyFileResult {
|
enum CopyFileResult {
|
||||||
SUCCESS = 0,
|
SUCCESS = 0,
|
||||||
|
|||||||
+90
-2
@@ -9,6 +9,8 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <mutex>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
@@ -702,13 +704,99 @@ namespace WindowsSupport
|
|||||||
std::error_code rename_file(const std::string &from, const std::string &to)
|
std::error_code rename_file(const std::string &from, const std::string &to)
|
||||||
{
|
{
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
|
// Retries and moves an open destination aside itself.
|
||||||
return WindowsSupport::rename(from, to);
|
return WindowsSupport::rename(from, to);
|
||||||
#else
|
#else
|
||||||
boost::nowide::remove(to.c_str());
|
// rename(2) replaces an existing target atomically; removing it first would
|
||||||
return std::make_error_code(static_cast<std::errc>(boost::nowide::rename(from.c_str(), to.c_str())));
|
// leave a window in which the file does not exist at all.
|
||||||
|
if (boost::nowide::rename(from.c_str(), to.c_str()) == 0)
|
||||||
|
return {};
|
||||||
|
const int err = errno;
|
||||||
|
// Some mounts (sshfs, gvfs, MTP and a few SMB setups) refuse to replace an
|
||||||
|
// existing target in one step, each with the error it sees fit; every error
|
||||||
|
// is worth the remove-then-rename this always did, except the ones no retry
|
||||||
|
// can help: nothing at the source, a different device, or a directory where
|
||||||
|
// a file was expected and the reverse.
|
||||||
|
const bool worth_retrying = err != ENOENT && err != EXDEV && err != ENOTDIR && err != EISDIR;
|
||||||
|
if (worth_retrying && boost::nowide::remove(to.c_str()) == 0 && boost::nowide::rename(from.c_str(), to.c_str()) == 0)
|
||||||
|
return {};
|
||||||
|
return std::make_error_code(static_cast<std::errc>(err));
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static std::error_code write_whole_file(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||||
|
{
|
||||||
|
errno = 0;
|
||||||
|
FILE *file = boost::nowide::fopen(path.c_str(), binary ? "wb" : "w");
|
||||||
|
if (file == nullptr)
|
||||||
|
return std::make_error_code(errno != 0 ? static_cast<std::errc>(errno) : std::errc::io_error);
|
||||||
|
bool ok = true;
|
||||||
|
for (const std::string_view chunk : chunks)
|
||||||
|
ok = ok && std::fwrite(chunk.data(), 1, chunk.size(), file) == chunk.size();
|
||||||
|
ok = ok && std::fflush(file) == 0;
|
||||||
|
const int err = ok ? 0 : errno;
|
||||||
|
ok = std::fclose(file) == 0 && ok;
|
||||||
|
if (ok)
|
||||||
|
return {};
|
||||||
|
return std::make_error_code(err != 0 ? static_cast<std::errc>(err) : std::errc::io_error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The in-place fallback truncates the target, so two threads of this process
|
||||||
|
// on the same file must not both be in it. One mutex for all such writes: they
|
||||||
|
// are the rare case. Never freed, like the InstanceLock registry, so a save
|
||||||
|
// during static destruction still finds it.
|
||||||
|
static std::error_code write_in_place(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||||
|
{
|
||||||
|
static auto *mutex = new std::mutex();
|
||||||
|
std::lock_guard<std::mutex> guard(*mutex);
|
||||||
|
return write_whole_file(path, chunks, binary);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
|
||||||
|
{
|
||||||
|
boost::system::error_code bec;
|
||||||
|
const boost::filesystem::file_status target = boost::filesystem::symlink_status(path, bec);
|
||||||
|
const bool target_exists = ! bec && boost::filesystem::exists(target);
|
||||||
|
if (target_exists && boost::filesystem::is_symlink(target)) {
|
||||||
|
// A config or preset kept in a dotfiles repository: the link stays,
|
||||||
|
// the file it points to is replaced like any other.
|
||||||
|
const boost::filesystem::path resolved = boost::filesystem::canonical(path, bec);
|
||||||
|
if (! bec && boost::filesystem::is_regular_file(resolved, bec))
|
||||||
|
return write_file_atomically(resolved.string(), chunks, binary);
|
||||||
|
}
|
||||||
|
if (target_exists && ! boost::filesystem::is_regular_file(target))
|
||||||
|
return write_in_place(path, chunks, binary);
|
||||||
|
|
||||||
|
// Unique per process and per call, so two threads writing one target
|
||||||
|
// without a lock never share a temporary.
|
||||||
|
static std::atomic<unsigned> counter{0};
|
||||||
|
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + "." + std::to_string(counter++) + ".tmp";
|
||||||
|
if (const std::error_code ec = write_whole_file(tmp_path, chunks, binary)) {
|
||||||
|
boost::nowide::remove(tmp_path.c_str());
|
||||||
|
if (! target_exists)
|
||||||
|
return ec;
|
||||||
|
// A directory that lets this process write its files but not create
|
||||||
|
// one: losing the save is worse than a reader seeing a partial file.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Cannot create a temporary beside " << path << " (" << ec.message() << "); writing in place";
|
||||||
|
return write_in_place(path, chunks, binary);
|
||||||
|
}
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Not on Windows, where a read-only bit on the temporary would stop the rename itself.
|
||||||
|
if (target_exists)
|
||||||
|
boost::filesystem::permissions(tmp_path, target.permissions(), bec);
|
||||||
|
#endif
|
||||||
|
if (const std::error_code ec = rename_file(tmp_path, path)) {
|
||||||
|
boost::nowide::remove(tmp_path.c_str());
|
||||||
|
// A reader on Windows holding the target open without FILE_SHARE_DELETE,
|
||||||
|
// or a mount that cannot replace a file at all. Losing the save is worse
|
||||||
|
// than a reader seeing a partial file, so write in place the way this
|
||||||
|
// used to work before the atomic path existed.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "Cannot replace " << path << " (" << ec.message() << "); writing in place";
|
||||||
|
return write_in_place(path, chunks, binary);
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef __linux__
|
#ifdef __linux__
|
||||||
// Copied from boost::filesystem.
|
// Copied from boost::filesystem.
|
||||||
// Called by copy_file_linux() in case linux sendfile() API is not supported.
|
// Called by copy_file_linux() in case linux sendfile() API is not supported.
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,8 +151,6 @@ DevFirmwareVersionInfo DevNozzle::GetFirmwareInfo() const
|
|||||||
int DevNozzle::GetLogicExtruderId() const
|
int DevNozzle::GetLogicExtruderId() const
|
||||||
{
|
{
|
||||||
int total_ext_count = GetTotalExtruderCount();
|
int total_ext_count = GetTotalExtruderCount();
|
||||||
|
|
||||||
if (GUI::wxGetApp().preset_bundle->is_bbl_vendor()) {
|
|
||||||
if (total_ext_count == 1) {
|
if (total_ext_count == 1) {
|
||||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||||
} else if (total_ext_count == 2) {
|
} else if (total_ext_count == 2) {
|
||||||
@@ -169,17 +165,8 @@ int DevNozzle::GetLogicExtruderId() const
|
|||||||
return LOGIC_UNIQUE_EXTRUDER_ID;
|
return LOGIC_UNIQUE_EXTRUDER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
// For some reason, BBL's logical extruder ID is inverted:
|
|
||||||
// physical extruder id = 0 (MAIN_EXTRUDER_ID) vs logical extruder id = 1 (LOGIC_R_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;
|
return MAIN_EXTRUDER_ID;
|
||||||
@@ -194,9 +181,6 @@ int DevNozzle::GetExtruderId() const
|
|||||||
return MAIN_EXTRUDER_ID;
|
return MAIN_EXTRUDER_ID;
|
||||||
}
|
}
|
||||||
|
|
||||||
return m_nozzle_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DevNozzle::AtLeftExtruder() const
|
bool DevNozzle::AtLeftExtruder() const
|
||||||
{
|
{
|
||||||
assert(GetTotalExtruderCount() == 2);
|
assert(GetTotalExtruderCount() == 2);
|
||||||
|
|||||||
@@ -1199,25 +1199,20 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+29
-14
@@ -85,6 +85,7 @@
|
|||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/I18N.hpp"
|
#include "libslic3r/I18N.hpp"
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
|
#include "libslic3r/InstanceLock.hpp"
|
||||||
#include "libslic3r/Thread.hpp"
|
#include "libslic3r/Thread.hpp"
|
||||||
#include "libslic3r/miniz_extension.hpp"
|
#include "libslic3r/miniz_extension.hpp"
|
||||||
#include "libslic3r/Utils.hpp"
|
#include "libslic3r/Utils.hpp"
|
||||||
@@ -857,7 +858,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 +871,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 +908,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 +3424,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);
|
||||||
@@ -3545,7 +3550,7 @@ bool GUI_App::on_init_inner()
|
|||||||
update_publish_status();
|
update_publish_status();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_post_initialized && app_config->dirty())
|
if (m_post_initialized && app_config->dirty() && app_config->save_due())
|
||||||
app_config->save();
|
app_config->save();
|
||||||
|
|
||||||
});
|
});
|
||||||
@@ -7639,8 +7644,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
|
|||||||
|
|
||||||
// Delete the bundle folder and bundle
|
// Delete the bundle folder and bundle
|
||||||
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
|
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
|
||||||
|
{
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::filesystem::remove_all(bundle_folder, ec);
|
boost::filesystem::remove_all(bundle_folder, ec);
|
||||||
|
}
|
||||||
|
|
||||||
preset_bundle->bundles.WriteLock();
|
preset_bundle->bundles.WriteLock();
|
||||||
preset_bundle->bundles.m_bundles.erase(bundle.id);
|
preset_bundle->bundles.m_bundles.erase(bundle.id);
|
||||||
@@ -8201,10 +8209,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;
|
||||||
}
|
}
|
||||||
@@ -8925,6 +8935,7 @@ void GUI_App::preset_deleted_from_cloud(std::string setting_id)
|
|||||||
|
|
||||||
// Delete the .info file after cloud deletion is confirmed
|
// Delete the .info file after cloud deletion is confirmed
|
||||||
if (!preset_file_path.empty() && fs::exists(fs::path(preset_file_path))) {
|
if (!preset_file_path.empty() && fs::exists(fs::path(preset_file_path))) {
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
boost::nowide::remove(preset_file_path.c_str());
|
boost::nowide::remove(preset_file_path.c_str());
|
||||||
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
|
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
|
||||||
}
|
}
|
||||||
@@ -8987,17 +8998,21 @@ void GUI_App::scan_orphaned_info_files()
|
|||||||
fs::path preset_file = info_file;
|
fs::path preset_file = info_file;
|
||||||
preset_file.replace_extension(".json");
|
preset_file.replace_extension(".json");
|
||||||
|
|
||||||
// If .json doesn't exist, .info is orphaned
|
// If .json doesn't exist, .info is orphaned. Read under the lock, so a
|
||||||
if (!fs::exists(preset_file)) {
|
// remove_files() in another instance is seen whole or not at all; the
|
||||||
// Extract setting_id from .info file
|
// delete queue's own mutex is taken after the lock is released.
|
||||||
std::string setting_id = extract_setting_id_from_info(info_file.string());
|
std::string setting_id;
|
||||||
|
{
|
||||||
|
InstanceLock instance_lock(user_presets_lock_path());
|
||||||
|
if (!fs::exists(preset_file))
|
||||||
|
setting_id = extract_setting_id_from_info(info_file.string());
|
||||||
|
}
|
||||||
if (!setting_id.empty()) {
|
if (!setting_id.empty()) {
|
||||||
// Add to need_delete_presets
|
// Add to need_delete_presets
|
||||||
delete_preset_from_cloud(setting_id, info_file.string());
|
delete_preset_from_cloud(setting_id, info_file.string());
|
||||||
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
|
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if (ec)
|
if (ec)
|
||||||
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
|
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, {
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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};
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
+121
-119
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static GLint logo_texture_size()
|
||||||
|
{
|
||||||
|
return std::min<GLint>(OpenGLManager::get_gl_info().get_max_tex_size(), 2048);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the texture of the next untried part across the parts of `infos`, in order, advancing
|
||||||
|
// `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;
|
||||||
|
}
|
||||||
|
|
||||||
void PartPlateList::load_bedtype_textures()
|
void PartPlateList::load_bedtype_textures()
|
||||||
{
|
{
|
||||||
if (PartPlateList::is_load_bedtype_textures) return;
|
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();
|
init_bed_type_info();
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
|
if (load_next_part_texture(bed_texture_info, btCount, m_next_bedtype_texture, true))
|
||||||
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
|
return true;
|
||||||
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;
|
PartPlateList::is_load_bedtype_textures = true;
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void PartPlateList::load_extruder_only_area_textures() {
|
bool PartPlateList::load_logo_texture()
|
||||||
if (PartPlateList::is_load_extruder_only_area_textures) return;
|
{
|
||||||
|
if (m_logo_texture_filename.empty()) {
|
||||||
|
m_logo_texture.reset();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
auto ok = init_extruder_only_area_info();
|
if (m_logo_texture.get_id() != 0 && m_logo_texture.get_source() == m_logo_texture_filename) {
|
||||||
if (!ok) {
|
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()) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PartPlateList::load_next_cali_texture()
|
||||||
|
{
|
||||||
|
if (PartPlateList::is_load_cali_texture)
|
||||||
|
return false;
|
||||||
|
if (m_next_cali_texture == 0)
|
||||||
init_cali_texture_info();
|
init_cali_texture_info();
|
||||||
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
|
if (load_next_part_texture(&cali_texture_info, 1, m_next_cali_texture, true))
|
||||||
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
|
return true;
|
||||||
for (int i = 0; i < (unsigned int)btCount; ++i) {
|
|
||||||
for (int j = 0; j < cali_texture_info.parts.size(); j++) {
|
|
||||||
std::string filename = resources_dir() + "/images/" + cali_texture_info.parts[j].filename;
|
|
||||||
if (boost::filesystem::exists(filename)) {
|
|
||||||
PartPlateList::cali_texture_info.parts[j].texture = new GLTexture();
|
|
||||||
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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+12
-189
@@ -1875,7 +1875,6 @@ 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);
|
||||||
@@ -1884,17 +1883,6 @@ void StatusBasePanel::reset_temp_misc_control()
|
|||||||
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);
|
||||||
m_tempCtrl_bed->GetTextCtrl()->SetValue(TEMP_BLANK_STR);
|
m_tempCtrl_bed->GetTextCtrl()->SetValue(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,10 +2463,6 @@ 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
|
|
||||||
// TempInput widgets with the same "type" id as m_tempCtrl_nozzle (as m_tempCtrl_nozzle_deputy
|
|
||||||
// already does today), so their events route into this branch too.
|
|
||||||
if (wxGetApp().preset_bundle->is_bbl_vendor()) {
|
|
||||||
if (e.GetString() == wxString::Format("%d", MAIN_EXTRUDER_ID)) {
|
if (e.GetString() == wxString::Format("%d", MAIN_EXTRUDER_ID)) {
|
||||||
on_set_nozzle_temp(MAIN_EXTRUDER_ID);
|
on_set_nozzle_temp(MAIN_EXTRUDER_ID);
|
||||||
} else if (e.GetString() == wxString::Format("%d", DEPUTY_EXTRUDER_ID)) {
|
} else if (e.GetString() == wxString::Format("%d", DEPUTY_EXTRUDER_ID)) {
|
||||||
@@ -2484,16 +2470,6 @@ void StatusPanel::wire_controls()
|
|||||||
} else {
|
} else {
|
||||||
on_set_nozzle_temp(UNIQUE_EXTRUDER_ID);//there is only one nozzle
|
on_set_nozzle_temp(UNIQUE_EXTRUDER_ID);//there is only one nozzle
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// N-extruder widgets all share TEMP_OF_NORMAL_TYPE, so e.GetString() can't
|
|
||||||
// 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()) {
|
||||||
m_tempCtrl_chamber->SetOnChanging();
|
m_tempCtrl_chamber->SetOnChanging();
|
||||||
@@ -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);
|
||||||
|
|
||||||
if (wxGetApp().preset_bundle->is_bbl_vendor()) {
|
|
||||||
m_tempCtrl_nozzle->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->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_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_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,8 +3095,6 @@ 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 (wxGetApp().preset_bundle->is_bbl_vendor()) {
|
|
||||||
if (nozzle_num == 1)
|
if (nozzle_num == 1)
|
||||||
{
|
{
|
||||||
m_tempCtrl_nozzle->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(MAIN_EXTRUDER_ID));
|
m_tempCtrl_nozzle->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent(MAIN_EXTRUDER_ID));
|
||||||
@@ -3186,21 +3126,7 @@ void StatusPanel::update_temp_ctrl(MachineObject *obj)
|
|||||||
m_tempCtrl_nozzle->SetMinSize(TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON);
|
m_tempCtrl_nozzle->SetMinSize(TEMP_CTRL_MIN_SIZE_ALIGN_TWO_ICON);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_tempCtrl_nozzle->Hide();
|
|
||||||
m_tempCtrl_nozzle_deputy->Hide();
|
|
||||||
|
|
||||||
for (size_t i = 0; i < m_tempCtrl_nozzles.size(); ++i)
|
|
||||||
{
|
|
||||||
m_tempCtrl_nozzles[i]->SetCurrTemp(obj->GetExtderSystem()->GetNozzleTempCurrent((int)i));
|
|
||||||
m_tempCtrl_nozzles[i]->SetCurrType(TEMP_OF_NORMAL_TYPE);
|
|
||||||
m_tempCtrl_nozzles[i]->Show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
||||||
@@ -3243,32 +3169,6 @@ void StatusPanel::update_temp_ctrl(MachineObject *obj)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else {
|
|
||||||
assert(m_temp_nozzle_timeouts.size() == m_tempCtrl_nozzles.size());
|
|
||||||
for (size_t i = 0; i < m_temp_nozzle_timeouts.size(); ++i) {
|
|
||||||
if (m_temp_nozzle_timeouts[i] > 0) {
|
|
||||||
m_temp_nozzle_timeouts[i]--;
|
|
||||||
} else {
|
|
||||||
if (!nozzle_temp_input) {
|
|
||||||
auto extder = obj->GetExtderSystem()->GetExtderById(i);
|
|
||||||
if (extder)
|
|
||||||
{
|
|
||||||
m_tempCtrl_nozzles[i]->SetTagTemp(extder->GetTargetTemp());
|
|
||||||
m_tempCtrl_nozzles[i]->SetCurrTemp((int)extder->GetCurrentTemp());
|
|
||||||
if (extder->GetTargetTemp() - extder->GetCurrentTemp() > TEMP_THRESHOLD_VAL)
|
|
||||||
{
|
|
||||||
m_tempCtrl_nozzles[i]->SetIconActive();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
m_tempCtrl_nozzles[i]->SetIconNormal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// support current temp for chamber
|
// support current temp for chamber
|
||||||
if (obj->get_printer_series() == PrinterSeries::SERIES_X1)
|
if (obj->get_printer_series() == PrinterSeries::SERIES_X1)
|
||||||
@@ -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,7 +3473,6 @@ 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);
|
||||||
@@ -3608,21 +3502,6 @@ void StatusPanel::update_ams(MachineObject *obj)
|
|||||||
}
|
}
|
||||||
m_ams_control->SetExtruder(ext->HasFilamentInExt(), DEPUTY_EXTRUDER_ID, 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);
|
||||||
}
|
}
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
update_filament_loading_panel(obj);
|
update_filament_loading_panel(obj);
|
||||||
|
|
||||||
@@ -4306,7 +4185,6 @@ 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) {
|
||||||
@@ -4336,20 +4214,6 @@ void StatusPanel::on_set_nozzle_temp(int nozzle_id)
|
|||||||
obj->command_set_nozzle_new(DEPUTY_EXTRUDER_ID, nozzle_temp);
|
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());
|
|
||||||
wxString str = m_tempCtrl_nozzles[nozzle_id]->GetTextCtrl()->GetValue();
|
|
||||||
if (str.ToLong(&nozzle_temp) && obj) {
|
|
||||||
set_hold_count(m_temp_nozzle_timeouts[nozzle_id]);
|
|
||||||
if (nozzle_temp > m_tempCtrl_nozzles[nozzle_id]->get_max_temp()) {
|
|
||||||
nozzle_temp = m_tempCtrl_nozzles[nozzle_id]->get_max_temp();
|
|
||||||
m_tempCtrl_nozzles[nozzle_id]->SetTagTemp(wxString::Format("%d", nozzle_temp));
|
|
||||||
m_tempCtrl_nozzles[nozzle_id]->Warning(false);
|
|
||||||
}
|
|
||||||
obj->command_set_nozzle_new(nozzle_id, nozzle_temp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
;
|
;
|
||||||
}
|
}
|
||||||
@@ -4431,17 +4295,10 @@ 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
|
|
||||||
// audited for N > 2 -- this only fixes what's local to StatusPanel.cpp.
|
|
||||||
int extruder_count;
|
|
||||||
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 (auto ext = obj->GetExtderSystem()->GetExtderById(MAIN_EXTRUDER_ID); ext.has_value())
|
||||||
{
|
{
|
||||||
if (ext->HasFilamentInExt())
|
if (ext->HasFilamentInExt())
|
||||||
@@ -4456,19 +4313,8 @@ void StatusPanel::on_ams_load_curr()
|
|||||||
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,7 +4416,6 @@ 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);
|
||||||
@@ -4588,21 +4433,7 @@ void StatusPanel::on_ams_switch(SimpleEvent &event)
|
|||||||
|
|
||||||
/*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->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(), DEPUTY_EXTRUDER_ID, ext->GetSlotNow().ams_id, ext->GetSlotNow().slot_id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
/*all extruders, in id order*/
|
|
||||||
for (const auto& ext : obj->GetExtderSystem()->GetExtruders()) {
|
|
||||||
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);
|
||||||
@@ -4612,8 +4443,7 @@ void StatusPanel::on_ams_switch(SimpleEvent &event)
|
|||||||
} 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_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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
+13
-2
@@ -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()
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -1405,23 +1405,26 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
// is served from the shipped profiles. Each is stamped by name and
|
// is served from the shipped profiles. Each is stamped by name and
|
||||||
// version alone: a profile change requires a version bump, so those two
|
// version alone: a profile change requires a version bump, so those two
|
||||||
// determine content wherever the vendor's copy sits.
|
// determine content wherever the vendor's copy sits.
|
||||||
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
|
std::vector<PresetBundle::VendorSource> ordered;
|
||||||
std::vector<VendorSource> ordered;
|
json stamps = json::array();
|
||||||
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
|
auto add_vendor = [&ordered, &stamps](const std::string& name, const boost::filesystem::path& dir) {
|
||||||
// The version a load from `dir` would serve: the profile's where one
|
// The version a load from `dir` would serve: the profile's where one
|
||||||
// exists (a cache is only served while it covers the profile beside
|
// exists (a cache is only served while it covers the profile beside
|
||||||
// it), the cache's own stamp where the cache is the whole vendor.
|
// it), the cache's own stamp where the cache is the whole vendor.
|
||||||
// A profile without a version (blacklist.json) carries no presets
|
// A profile without a version (blacklist.json) carries no presets
|
||||||
// and is passed over.
|
// and is passed over.
|
||||||
const boost::filesystem::path profile = dir / (name + ".json");
|
const boost::filesystem::path profile = dir / (name + ".json");
|
||||||
|
std::string version;
|
||||||
if (boost::filesystem::exists(profile)) {
|
if (boost::filesystem::exists(profile)) {
|
||||||
const Semver v = get_version_from_json(profile.string());
|
const Semver v = get_version_from_json(profile.string());
|
||||||
if (v.valid())
|
if (! v.valid())
|
||||||
ordered.push_back({name, dir, v.to_string()});
|
return;
|
||||||
|
version = v.to_string();
|
||||||
} else {
|
} else {
|
||||||
ordered.push_back({name, dir,
|
version = VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name);
|
||||||
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
|
|
||||||
}
|
}
|
||||||
|
ordered.push_back({name, dir});
|
||||||
|
stamps.push_back({name, version});
|
||||||
};
|
};
|
||||||
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
const std::string filament_library(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||||
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
|
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
|
||||||
@@ -1431,9 +1434,6 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
add_vendor(name, dir);
|
add_vendor(name, dir);
|
||||||
if (ordered.empty())
|
if (ordered.empty())
|
||||||
return false;
|
return false;
|
||||||
json stamps = json::array();
|
|
||||||
for (const VendorSource& v : ordered)
|
|
||||||
stamps.push_back({v.name, v.version});
|
|
||||||
|
|
||||||
// What this function derives is a pure function of that stamped set, so
|
// What this function derives is a pure function of that stamped set, so
|
||||||
// the derived JSON is cached whole: a fresh cache makes an open one
|
// the derived JSON is cached whole: a fresh cache makes an open one
|
||||||
@@ -1461,34 +1461,24 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Each vendor comes from its preset cache where one covers it, which is
|
// Each vendor comes from its preset cache where one covers it, which is
|
||||||
// what makes this worth doing instead of the scan below; loading into a
|
// what makes this worth doing instead of the scan below.
|
||||||
// bundle per vendor keeps the install order the startup path has.
|
|
||||||
PresetBundle bundle;
|
PresetBundle bundle;
|
||||||
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
|
std::vector<std::string> failed;
|
||||||
const boost::filesystem::path& dir, const PresetBundle* base) {
|
const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||||
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
|
/*allow_cache=*/true, m_cancel_token.get(), &failed).second;
|
||||||
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
|
if (*m_cancel_token || bundle.vendors.empty())
|
||||||
};
|
return false;
|
||||||
for (const VendorSource& v : ordered) {
|
if (! errors.empty())
|
||||||
if (*m_cancel_token)
|
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors;
|
||||||
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
|
// A vendor that failed to load sends this open to the scan below, which lists
|
||||||
if (v.name == filament_library) {
|
// what it can read of every vendor.
|
||||||
load_vendor(bundle, v.name, v.dir, nullptr);
|
if (! failed.empty())
|
||||||
} else {
|
|
||||||
PresetBundle tmp;
|
|
||||||
load_vendor(tmp, v.name, v.dir, &bundle);
|
|
||||||
bundle.merge_presets(std::move(tmp));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (bundle.vendors.empty())
|
|
||||||
return false;
|
return false;
|
||||||
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
|
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
// Written through a temp file and moved into place, as the preset caches
|
// Written through a temp file and moved into place, as the preset caches
|
||||||
// are: half a cache must never be readable, and the PID suffix keeps two
|
// are: half a cache must never be readable.
|
||||||
// instances from interleaving on one temp file.
|
|
||||||
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
|
|
||||||
try {
|
try {
|
||||||
json out;
|
json out;
|
||||||
out["format"] = 1;
|
out["format"] = 1;
|
||||||
@@ -1497,18 +1487,9 @@ bool GuideFrame::BuildProfileDataFromVendors()
|
|||||||
for (const char* key : { "model", "machine", "filament", "process" })
|
for (const char* key : { "model", "machine", "filament", "process" })
|
||||||
profile[key] = m_ProfileJson[key];
|
profile[key] = m_ProfileJson[key];
|
||||||
boost::filesystem::create_directories(cache_file.parent_path());
|
boost::filesystem::create_directories(cache_file.parent_path());
|
||||||
{
|
if (const std::error_code ec = write_file_atomically(cache_file.string(), out.dump(-1, ' ', false, json::error_handler_t::ignore), /*binary=*/true))
|
||||||
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
|
|
||||||
ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore);
|
|
||||||
ofs.close();
|
|
||||||
if (! ofs.good())
|
|
||||||
throw std::runtime_error("write failed");
|
|
||||||
}
|
|
||||||
if (const std::error_code ec = rename_file(tmp_path, cache_file.string()))
|
|
||||||
throw std::runtime_error(ec.message());
|
throw std::runtime_error(ec.message());
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
boost::system::error_code rm;
|
|
||||||
boost::filesystem::remove(tmp_path, rm);
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
|
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -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 (wxGetApp().preset_bundle->is_bbl_vendor()) {
|
|
||||||
extruder_id = nozzle.AtLeftExtruder() ? 0 : 1;
|
|
||||||
if (nozzle.AtRightExtruder())
|
if (nozzle.AtRightExtruder())
|
||||||
has_right = true;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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__
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
#include <system_error>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <boost/format.hpp>
|
#include <boost/format.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
@@ -580,9 +581,10 @@ bool C3DPrinterOS::save_api_session(const std::string &session, const std::strin
|
|||||||
j.put("session", session);
|
j.put("session", session);
|
||||||
j.put("email", email);
|
j.put("email", email);
|
||||||
try {
|
try {
|
||||||
auto temp_path = m_api_session_file_path + ".tmp";
|
std::ostringstream json;
|
||||||
pt::write_json(temp_path, j);
|
pt::write_json(json, j);
|
||||||
boost::filesystem::rename(temp_path, m_api_session_file_path);
|
if (const std::error_code ec = write_file_atomically(m_api_session_file_path, json.str()))
|
||||||
|
throw std::system_error(ec);
|
||||||
} catch (const std::exception &err) {
|
} catch (const std::exception &err) {
|
||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
|
||||||
<< m_api_session_file_path
|
<< m_api_session_file_path
|
||||||
|
|||||||
@@ -1475,15 +1475,8 @@ void OrcaCloudServiceAgent::save_sync_state()
|
|||||||
if (sync_state_path.empty())
|
if (sync_state_path.empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
try {
|
if (const std::error_code ec = write_file_atomically(sync_state_path, std::to_string(sync_state.last_sync_timestamp)))
|
||||||
std::string tmp_path = sync_state_path + ".tmp";
|
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to save the sync state: " << ec.message();
|
||||||
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc);
|
|
||||||
if (ofs.good()) {
|
|
||||||
ofs << std::to_string(sync_state.last_sync_timestamp);
|
|
||||||
ofs.close();
|
|
||||||
boost::filesystem::rename(tmp_path, sync_state_path);
|
|
||||||
}
|
|
||||||
} catch (...) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void OrcaCloudServiceAgent::clear_sync_state()
|
void OrcaCloudServiceAgent::clear_sync_state()
|
||||||
@@ -1572,22 +1565,10 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
|
|||||||
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
|
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string tmp_path = secret_fallback_path + ".tmp";
|
if (const std::error_code ec = write_file_atomically(secret_fallback_path, signed_payload, /*binary=*/true))
|
||||||
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary);
|
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot write user secret file " << secret_fallback_path << ": " << ec.message();
|
||||||
if (ofs.good()) {
|
else
|
||||||
ofs << signed_payload;
|
|
||||||
ofs.flush();
|
|
||||||
ofs.close();
|
|
||||||
|
|
||||||
if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(secret_fallback_path.c_str()), true)) {
|
|
||||||
stored = true;
|
stored = true;
|
||||||
} else {
|
|
||||||
wxRemoveFile(wxString::FromUTF8(tmp_path.c_str()));
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace user secret file";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open user secret file for write - " << secret_fallback_path;
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Use wxSecretStore only
|
// Use wxSecretStore only
|
||||||
wxSecretStore store = wxSecretStore::GetDefault();
|
wxSecretStore store = wxSecretStore::GetDefault();
|
||||||
|
|||||||
@@ -249,21 +249,10 @@ bool PluginConfig::save()
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot truncate an
|
// Written beside the target and moved into place, so a crash mid-write cannot truncate an
|
||||||
// existing config. Same approach as AppConfig::save().
|
// existing config.
|
||||||
const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
|
if (const std::error_code ec = write_file_atomically(path, root.dump(1, '\t') + "\n")) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path << ": " << ec.message() << "; keeping the existing config";
|
||||||
boost::nowide::ofstream file;
|
|
||||||
file.open(path_pid, std::ios::out | std::ios::trunc);
|
|
||||||
file << root.dump(1, '\t') << std::endl;
|
|
||||||
file.close();
|
|
||||||
if (file.fail()) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path_pid << "; keeping the existing config";
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (const std::error_code rename_ec = rename_file(path_pid, path)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to move " << path_pid << " onto " << path << ": " << rename_ec.message();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 = [®ion, 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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -14,7 +14,9 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_clipper_utils.cpp
|
test_clipper_utils.cpp
|
||||||
test_config.cpp
|
test_config.cpp
|
||||||
test_config_variant_expansion.cpp
|
test_config_variant_expansion.cpp
|
||||||
|
test_locales_utils.cpp
|
||||||
test_toolordering_nozzle_group.cpp
|
test_toolordering_nozzle_group.cpp
|
||||||
|
test_parallel_resolve.cpp
|
||||||
test_preset_bundle_loading.cpp
|
test_preset_bundle_loading.cpp
|
||||||
test_preset_setting_id.cpp
|
test_preset_setting_id.cpp
|
||||||
test_preset_diff.cpp
|
test_preset_diff.cpp
|
||||||
@@ -49,6 +51,7 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_ordering_strategies.cpp
|
test_ordering_strategies.cpp
|
||||||
# test_png_io.cpp
|
# test_png_io.cpp
|
||||||
test_indexed_triangle_set.cpp
|
test_indexed_triangle_set.cpp
|
||||||
|
test_instance_lock.cpp
|
||||||
../libnest2d/printer_parts.cpp
|
../libnest2d/printer_parts.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <atomic>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
#include <boost/filesystem.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/InstanceLock.hpp"
|
||||||
|
#include "test_utils.hpp"
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#endif
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
using namespace std::chrono_literals;
|
||||||
|
|
||||||
|
// Sets a process-wide knob for one test and restores it however the test ends.
|
||||||
|
template<typename T> struct ScopedStaticValue
|
||||||
|
{
|
||||||
|
T &ref;
|
||||||
|
T saved;
|
||||||
|
ScopedStaticValue(T &ref, T value) : ref(ref), saved(ref) { ref = value; }
|
||||||
|
~ScopedStaticValue() { ref = saved; }
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock creates its lock file and holds it for the guard's scope", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
|
||||||
|
{
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
REQUIRE(boost::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
// Released: a fresh guard gets the lock at once instead of waiting out a timeout.
|
||||||
|
const auto started = std::chrono::steady_clock::now();
|
||||||
|
InstanceLock again(path, 5000ms);
|
||||||
|
REQUIRE(again.locked());
|
||||||
|
// Well inside the timeout it would otherwise have waited out; loose enough for a loaded runner.
|
||||||
|
REQUIRE(std::chrono::steady_clock::now() - started < 4000ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock nests within one thread", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
|
||||||
|
InstanceLock outer(path);
|
||||||
|
{
|
||||||
|
InstanceLock inner(path, 100ms);
|
||||||
|
REQUIRE(inner.locked());
|
||||||
|
}
|
||||||
|
// The inner guard leaving does not release the outer one.
|
||||||
|
REQUIRE(outer.locked());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock is a no-op for an empty path and survives an unwritable one", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
|
||||||
|
InstanceLock none("");
|
||||||
|
REQUIRE_FALSE(none.locked());
|
||||||
|
|
||||||
|
// The directory does not exist, so the lock file cannot be created; the
|
||||||
|
// guard still constructs and the write it guards can go ahead.
|
||||||
|
InstanceLock unwritable((dir.path() / "missing" / "shared.lock").string(), 100ms);
|
||||||
|
REQUIRE_FALSE(unwritable.locked());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock retries a lock file it could not open once the cool-down passes", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const std::string path = (dir.path() / "later" / "shared.lock").string();
|
||||||
|
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
|
||||||
|
|
||||||
|
bool before_dir, during_cooldown, after_cooldown;
|
||||||
|
const auto started = std::chrono::steady_clock::now();
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
before_dir = lock.locked();
|
||||||
|
}
|
||||||
|
boost::filesystem::create_directories(dir.path() / "later");
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
during_cooldown = lock.locked();
|
||||||
|
}
|
||||||
|
const bool second_guard_inside_cooldown = std::chrono::steady_clock::now() - started < InstanceLock::cooldown;
|
||||||
|
std::this_thread::sleep_for(400ms);
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
after_cooldown = lock.locked();
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE_FALSE(before_dir);
|
||||||
|
// A loaded runner may take longer than the cool-down to get here; then the
|
||||||
|
// second guard legitimately retried, so only assert when the timing held.
|
||||||
|
if (second_guard_inside_cooldown)
|
||||||
|
REQUIRE_FALSE(during_cooldown);
|
||||||
|
REQUIRE(after_cooldown);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock reopens a lock file that was replaced on disk", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
{
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
}
|
||||||
|
|
||||||
|
boost::filesystem::remove(path);
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
// Each outermost guard opens the file afresh, so the deleted path is back.
|
||||||
|
REQUIRE(boost::filesystem::exists(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
|
||||||
|
std::atomic<bool> holder_ready{false};
|
||||||
|
std::atomic<bool> holder_released{false};
|
||||||
|
std::thread holder([&] {
|
||||||
|
InstanceLock lock(path);
|
||||||
|
holder_ready = true;
|
||||||
|
std::this_thread::sleep_for(150ms);
|
||||||
|
holder_released = true;
|
||||||
|
});
|
||||||
|
while (! holder_ready)
|
||||||
|
std::this_thread::yield();
|
||||||
|
|
||||||
|
bool released_before_acquire = false;
|
||||||
|
{
|
||||||
|
InstanceLock lock(path);
|
||||||
|
released_before_acquire = holder_released;
|
||||||
|
}
|
||||||
|
holder.join();
|
||||||
|
REQUIRE(released_before_acquire);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// The cross-process side of the lock is a POSIX flock, which a child process
|
||||||
|
// takes here directly; LockFileEx backs the guard on Windows, but spawning a
|
||||||
|
// child there is not worth a test.
|
||||||
|
TEST_CASE("InstanceLock yields to another process and reports it", "[InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryFile lock_file(".lock");
|
||||||
|
const std::string path = lock_file.string();
|
||||||
|
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
|
||||||
|
|
||||||
|
int child_holds[2], child_may_exit[2];
|
||||||
|
REQUIRE(::pipe(child_holds) == 0);
|
||||||
|
REQUIRE(::pipe(child_may_exit) == 0);
|
||||||
|
|
||||||
|
const pid_t child = ::fork();
|
||||||
|
REQUIRE(child >= 0);
|
||||||
|
if (child == 0) {
|
||||||
|
int fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
|
||||||
|
char byte = ::flock(fd, LOCK_EX | LOCK_NB) == 0 ? '1' : '0';
|
||||||
|
if (::write(child_holds[1], &byte, 1) != 1 || ::read(child_may_exit[0], &byte, 1) != 1)
|
||||||
|
::_exit(1);
|
||||||
|
::_exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
char byte = '0';
|
||||||
|
REQUIRE(::read(child_holds[0], &byte, 1) == 1);
|
||||||
|
REQUIRE(byte == '1');
|
||||||
|
|
||||||
|
bool locked_while_child_holds;
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 100ms);
|
||||||
|
locked_while_child_holds = lock.locked();
|
||||||
|
}
|
||||||
|
// The timed-out wait starts a cool-down: the next guard does not touch the file.
|
||||||
|
const auto started = std::chrono::steady_clock::now();
|
||||||
|
bool locked_during_cooldown;
|
||||||
|
{
|
||||||
|
InstanceLock lock(path, 5000ms);
|
||||||
|
locked_during_cooldown = lock.locked();
|
||||||
|
}
|
||||||
|
const auto cooldown_wait = std::chrono::steady_clock::now() - started;
|
||||||
|
REQUIRE(::write(child_may_exit[1], "x", 1) == 1);
|
||||||
|
int status = 0;
|
||||||
|
REQUIRE(::waitpid(child, &status, 0) == child);
|
||||||
|
for (int fd : {child_holds[0], child_holds[1], child_may_exit[0], child_may_exit[1]})
|
||||||
|
::close(fd);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(locked_while_child_holds);
|
||||||
|
REQUIRE_FALSE(locked_during_cooldown);
|
||||||
|
REQUIRE(cooldown_wait < 4000ms);
|
||||||
|
// Once the cool-down passes, the lock the child released is taken again.
|
||||||
|
std::this_thread::sleep_for(400ms);
|
||||||
|
InstanceLock lock(path);
|
||||||
|
REQUIRE(lock.locked());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <clocale>
|
||||||
|
|
||||||
|
#include "libslic3r/LocalesUtils.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
// Switches this thread's numeric locale to one whose decimal separator is a comma,
|
||||||
|
// when the system has one installed.
|
||||||
|
struct CommaNumericLocale
|
||||||
|
{
|
||||||
|
#ifdef _WIN32
|
||||||
|
bool apply()
|
||||||
|
{
|
||||||
|
for (const char* name : { "de-DE", "German_Germany.1252" })
|
||||||
|
if (std::setlocale(LC_NUMERIC, name) != nullptr)
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
locale_t locale { (locale_t) 0 };
|
||||||
|
bool apply()
|
||||||
|
{
|
||||||
|
for (const char* name : { "de_DE.UTF-8", "de_DE.utf8", "de_DE" })
|
||||||
|
if ((locale = newlocale(LC_NUMERIC_MASK, name, (locale_t) 0)) != (locale_t) 0) {
|
||||||
|
uselocale(locale);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Freed once the setters around apply() have put the thread's own locale back.
|
||||||
|
~CommaNumericLocale()
|
||||||
|
{
|
||||||
|
if (locale != (locale_t) 0)
|
||||||
|
freelocale(locale);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("a setter nested in another leaves the C locale in place for the outer one", "[LocalesUtils]")
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter outer;
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter inner;
|
||||||
|
CHECK(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
CHECK(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a setter nested in another sets C again when the locale changed between them", "[LocalesUtils]")
|
||||||
|
{
|
||||||
|
CommaNumericLocale comma;
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter outer;
|
||||||
|
if (! comma.apply())
|
||||||
|
SKIP("no locale with a comma decimal separator is installed");
|
||||||
|
REQUIRE_FALSE(is_decimal_separator_point());
|
||||||
|
{
|
||||||
|
CNumericLocalesSetter inner;
|
||||||
|
CHECK(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
CHECK_FALSE(is_decimal_separator_point());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <numeric>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <thread>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <tbb/blocked_range.h>
|
||||||
|
#include <tbb/parallel_for.h>
|
||||||
|
#include <tbb/task_group.h>
|
||||||
|
|
||||||
|
#include "libslic3r/ParallelResolve.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
thread_local int t_live_setups = 0;
|
||||||
|
|
||||||
|
// Counts how many are alive on the constructing thread.
|
||||||
|
struct CountingSetup
|
||||||
|
{
|
||||||
|
CountingSetup() { ++ t_live_setups; }
|
||||||
|
~CountingSetup() { -- t_live_setups; }
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<size_t> first_indices(size_t count)
|
||||||
|
{
|
||||||
|
std::vector<size_t> indices(count);
|
||||||
|
std::iota(indices.begin(), indices.end(), size_t(0));
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("every item resolves once and commits in index order on the calling thread", "[ParallelResolve]")
|
||||||
|
{
|
||||||
|
const size_t count = 200;
|
||||||
|
std::vector<int> resolves(count, 0);
|
||||||
|
std::vector<size_t> committed, values;
|
||||||
|
std::vector<bool> on_caller;
|
||||||
|
const std::thread::id caller = std::this_thread::get_id();
|
||||||
|
resolve_then_commit(count,
|
||||||
|
[&](size_t i) { ++ resolves[i]; return i * 3; },
|
||||||
|
[&](size_t i, size_t resolved) {
|
||||||
|
committed.push_back(i);
|
||||||
|
values.push_back(resolved);
|
||||||
|
on_caller.push_back(std::this_thread::get_id() == caller);
|
||||||
|
});
|
||||||
|
|
||||||
|
CHECK(committed == first_indices(count));
|
||||||
|
for (size_t i = 0; i < count; ++ i) {
|
||||||
|
CHECK(resolves[i] == 1);
|
||||||
|
CHECK(values[i] == i * 3);
|
||||||
|
CHECK(on_caller[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("every item resolves inside one chunk setup", "[ParallelResolve]")
|
||||||
|
{
|
||||||
|
const size_t count = 200;
|
||||||
|
std::vector<int> live(count, 0);
|
||||||
|
resolve_then_commit<CountingSetup>(count,
|
||||||
|
[&](size_t i) { live[i] = t_live_setups; return 0; },
|
||||||
|
[](size_t, int) {});
|
||||||
|
|
||||||
|
for (size_t i = 0; i < count; ++ i)
|
||||||
|
CHECK(live[i] == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("an exception from resolve leaves the batches before it committed", "[ParallelResolve]")
|
||||||
|
{
|
||||||
|
const size_t count = 200, fails_at = 150;
|
||||||
|
REQUIRE(fails_at >= resolve_batch_size);
|
||||||
|
std::vector<size_t> committed;
|
||||||
|
CHECK_THROWS_AS(resolve_then_commit(count,
|
||||||
|
[&](size_t i) {
|
||||||
|
if (i == fails_at)
|
||||||
|
throw std::runtime_error("resolve failed");
|
||||||
|
return i;
|
||||||
|
},
|
||||||
|
[&](size_t i, size_t) { committed.push_back(i); }),
|
||||||
|
std::runtime_error);
|
||||||
|
|
||||||
|
CHECK(committed == first_indices(fails_at / resolve_batch_size * resolve_batch_size));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("an exception from commit stops at the item that threw", "[ParallelResolve]")
|
||||||
|
{
|
||||||
|
const size_t count = 200, fails_at = 90;
|
||||||
|
std::vector<size_t> committed;
|
||||||
|
CHECK_THROWS_AS(resolve_then_commit(count,
|
||||||
|
[](size_t i) { return i; },
|
||||||
|
[&](size_t i, size_t) {
|
||||||
|
if (i == fails_at)
|
||||||
|
throw std::runtime_error("commit failed");
|
||||||
|
committed.push_back(i);
|
||||||
|
}),
|
||||||
|
std::runtime_error);
|
||||||
|
|
||||||
|
CHECK(committed == first_indices(fails_at));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a canceled task group stops before committing an item it did not resolve", "[ParallelResolve]")
|
||||||
|
{
|
||||||
|
std::vector<size_t> committed;
|
||||||
|
bool threw = false;
|
||||||
|
tbb::task_group_context context;
|
||||||
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, 1), [&](const tbb::blocked_range<size_t>&) {
|
||||||
|
context.cancel_group_execution();
|
||||||
|
try {
|
||||||
|
resolve_then_commit(200,
|
||||||
|
[](size_t i) { return i + 1; },
|
||||||
|
[&](size_t, size_t resolved) { committed.push_back(resolved); });
|
||||||
|
} catch (const std::runtime_error&) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
}, context);
|
||||||
|
|
||||||
|
CHECK(threw);
|
||||||
|
CHECK(committed.empty());
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
|
#include "libslic3r/ParallelResolve.hpp"
|
||||||
#include "libslic3r/AppConfig.hpp"
|
#include "libslic3r/AppConfig.hpp"
|
||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/TriangleMesh.hpp"
|
#include "libslic3r/TriangleMesh.hpp"
|
||||||
@@ -17,6 +18,10 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <initializer_list>
|
#include <initializer_list>
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
#include <unistd.h> // geteuid
|
||||||
|
#endif
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -59,6 +64,16 @@ void write_preset_with_inherits(const DynamicPrintConfig &default_config, const
|
|||||||
config.save_to_json(file.string(), name, "User", "1.0.0");
|
config.save_to_json(file.string(), name, "User", "1.0.0");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A user preset file stating nothing but the preset it inherits, so every value it
|
||||||
|
// ends up with came from resolving that parent.
|
||||||
|
void write_minimal_child(const fs::path &file, const std::string &name, const std::string &inherits)
|
||||||
|
{
|
||||||
|
fs::create_directories(file.parent_path());
|
||||||
|
std::ofstream(file.string())
|
||||||
|
<< R"({"type":"process","name":")" << name << R"(","from":"User","version":"1.0.0","inherits":")"
|
||||||
|
<< inherits << R"("})";
|
||||||
|
}
|
||||||
|
|
||||||
// Add an in-memory preset (no file) with the given inherits value (empty => root preset).
|
// Add an in-memory preset (no file) with the given inherits value (empty => root preset).
|
||||||
Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {})
|
Preset &add_inmemory_preset(PresetCollection &coll, const std::string &name, const std::string &inherits = {})
|
||||||
{
|
{
|
||||||
@@ -261,6 +276,162 @@ TEST_CASE("Selected printer uses its default or saved bed type", "[Preset][Bundl
|
|||||||
CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast<int>(expected_bed_type)));
|
CHECK(app_config.get_printer_setting("Test Printer", "curr_bed_type") == std::to_string(static_cast<int>(expected_bed_type)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A directory of user presets loads with each one resolved against its parent", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
RenameTestCollection coll;
|
||||||
|
|
||||||
|
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||||
|
parent.config.option<ConfigOptionFloat>("layer_height", true)->value = 0.24;
|
||||||
|
parent.is_system = true;
|
||||||
|
|
||||||
|
constexpr int children = 400;
|
||||||
|
for (int i = 0; i < children; ++ i)
|
||||||
|
write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"),
|
||||||
|
"Child " + std::to_string(i), "Parent Process");
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Disable);
|
||||||
|
|
||||||
|
CHECK(coll.size() == size_t(children) + 2); // the children, the default preset and the parent
|
||||||
|
CHECK(coll.error_count() == 0);
|
||||||
|
for (int i = 0; i < children; ++ i) {
|
||||||
|
const Preset *child = coll.find_preset("Child " + std::to_string(i));
|
||||||
|
REQUIRE(child != nullptr);
|
||||||
|
CHECK(child->inherits() == "Parent Process");
|
||||||
|
CHECK(child->alias == "Child " + std::to_string(i));
|
||||||
|
CHECK(child->loaded);
|
||||||
|
REQUIRE(child->config.option<ConfigOptionFloat>("layer_height") != nullptr);
|
||||||
|
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(0.24, 1e-9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Repeated loads of a user preset directory produce the same presets", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
|
||||||
|
auto seed_directory = [&]() {
|
||||||
|
for (int i = 0; i < 200; ++ i)
|
||||||
|
write_minimal_child(temp_dir.path() / PRESET_PRINT_NAME / ("Child " + std::to_string(i) + ".json"),
|
||||||
|
"Child " + std::to_string(i), "Parent Process");
|
||||||
|
};
|
||||||
|
|
||||||
|
std::vector<std::vector<std::string>> names_per_run;
|
||||||
|
for (int run = 0; run < 3; ++ run) {
|
||||||
|
RenameTestCollection coll;
|
||||||
|
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||||
|
parent.is_system = true;
|
||||||
|
if (run == 0)
|
||||||
|
seed_directory();
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Disable);
|
||||||
|
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (auto it = coll.begin(); it != coll.end(); ++ it)
|
||||||
|
names.push_back(it->name + "|" + it->alias + "|" + it->inherits());
|
||||||
|
names_per_run.push_back(std::move(names));
|
||||||
|
}
|
||||||
|
|
||||||
|
REQUIRE(names_per_run[0].size() > 200);
|
||||||
|
CHECK(names_per_run[1] == names_per_run[0]);
|
||||||
|
CHECK(names_per_run[2] == names_per_run[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An unreadable user preset is counted and removed while the rest still load", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
RenameTestCollection coll;
|
||||||
|
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||||
|
|
||||||
|
for (int i = 0; i < 20; ++ i)
|
||||||
|
write_preset_with_inherits(coll.default_preset().config, dir / ("Good " + std::to_string(i) + ".json"),
|
||||||
|
"Good " + std::to_string(i), std::string());
|
||||||
|
fs::create_directories(dir);
|
||||||
|
std::ofstream((dir / "Broken.json").string()) << "{not-json";
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
|
||||||
|
CHECK(coll.error_count() == 1);
|
||||||
|
CHECK(coll.find_preset("Broken") == nullptr);
|
||||||
|
CHECK_FALSE(fs::exists(dir / "Broken.json"));
|
||||||
|
for (int i = 0; i < 20; ++ i)
|
||||||
|
CHECK(coll.find_preset("Good " + std::to_string(i)) != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A user filament naming no compatible printer gets the one after its @, in memory and on disk", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
PresetBundle bundle;
|
||||||
|
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
|
||||||
|
REQUIRE(bundle.filaments.default_preset().config.option<ConfigOptionStrings>("compatible_printers")->values.empty());
|
||||||
|
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
|
||||||
|
const std::vector<std::string> expected { "Test Printer" };
|
||||||
|
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
|
||||||
|
REQUIRE(preset != nullptr);
|
||||||
|
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == expected);
|
||||||
|
|
||||||
|
DynamicPrintConfig saved;
|
||||||
|
std::map<std::string, std::string> key_values;
|
||||||
|
std::string reason;
|
||||||
|
saved.load_from_json(file.string(), ForwardCompatibilitySubstitutionRule::EnableSilent, key_values, reason);
|
||||||
|
REQUIRE(reason.empty());
|
||||||
|
REQUIRE(saved.option<ConfigOptionStrings>("compatible_printers") != nullptr);
|
||||||
|
CHECK(saved.option<ConfigOptionStrings>("compatible_printers")->values == expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A user preset's setting id equal to its base id is dropped in memory, not in the .info written back", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
PresetBundle bundle;
|
||||||
|
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
|
||||||
|
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
|
||||||
|
fs::path info = file;
|
||||||
|
info.replace_extension(".info");
|
||||||
|
std::ofstream(info.string()) << "sync_info = \nuser_id = \nsetting_id = PFUS1\nbase_id = PFUS1\nupdated_time = 0\n";
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
|
||||||
|
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
|
||||||
|
REQUIRE(preset != nullptr);
|
||||||
|
CHECK(preset->setting_id.empty());
|
||||||
|
CHECK(preset->base_id == "PFUS1");
|
||||||
|
Preset reloaded(Preset::TYPE_FILAMENT, "My PLA @Test Printer");
|
||||||
|
reloaded.load_info(info.string());
|
||||||
|
CHECK(reloaded.setting_id == "PFUS1");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A user preset that is not loaded still reports its substituted values", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
RenameTestCollection coll;
|
||||||
|
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||||
|
fs::create_directories(dir);
|
||||||
|
std::ofstream((dir / "Orphan.json").string())
|
||||||
|
<< R"({"type":"process","name":"Orphan","from":"User","version":"1.0.0","inherits":"No Such Parent",)"
|
||||||
|
<< R"("wall_generator":"no_such_generator"})";
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Enable);
|
||||||
|
|
||||||
|
CHECK(coll.find_preset("Orphan") == nullptr);
|
||||||
|
CHECK(coll.error_count() == 1);
|
||||||
|
REQUIRE(substitutions.size() == 1);
|
||||||
|
CHECK(substitutions.front().preset_name == "Orphan");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]")
|
TEST_CASE("find_preset resolves a system preset's renamed_from", "[Preset][Rename]")
|
||||||
{
|
{
|
||||||
RenameTestCollection coll;
|
RenameTestCollection coll;
|
||||||
@@ -5482,3 +5653,214 @@ TEST_CASE("Config import confines zip entries, preset names and bundle ids to th
|
|||||||
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
CHECK_FALSE(any_filename_contains(temp_dir.path(), "bundle-escape"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A user preset saved over by another instance while its directory loads is read again under the lock", "[Preset][Bundle][InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data_dir_scope(temp_dir.path());
|
||||||
|
RenameTestCollection coll;
|
||||||
|
|
||||||
|
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||||
|
parent.config.option<ConfigOptionFloat>("layer_height", true)->value = 0.24;
|
||||||
|
parent.is_system = true;
|
||||||
|
|
||||||
|
// Fewer than one batch, so every file is read before the first one is committed.
|
||||||
|
constexpr int children = 8;
|
||||||
|
static_assert(children <= int(resolve_batch_size));
|
||||||
|
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||||
|
for (int i = 0; i < children; ++ i)
|
||||||
|
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
|
||||||
|
|
||||||
|
// The first commit stands in for another instance saving every other preset after
|
||||||
|
// they were read and before they are committed.
|
||||||
|
std::string first;
|
||||||
|
auto save_the_others = [&](Preset &preset) {
|
||||||
|
if (! first.empty())
|
||||||
|
return;
|
||||||
|
first = preset.name;
|
||||||
|
for (int i = 0; i < children; ++ i) {
|
||||||
|
const std::string name = "Child " + std::to_string(i);
|
||||||
|
if (name != first)
|
||||||
|
std::ofstream((dir / (name + ".json")).string())
|
||||||
|
<< R"({"type":"process","name":")" << name
|
||||||
|
<< R"(","from":"User","version":"1.0.0","inherits":"Parent Process","layer_height":"0.3"})";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Disable, save_the_others);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(first.empty());
|
||||||
|
CHECK(coll.error_count() == 0);
|
||||||
|
for (int i = 0; i < children; ++ i) {
|
||||||
|
const std::string name = "Child " + std::to_string(i);
|
||||||
|
const Preset *child = coll.find_preset(name);
|
||||||
|
REQUIRE(child != nullptr);
|
||||||
|
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(name == first ? 0.24 : 0.3, 1e-9));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A user preset removed by another instance while its directory loads is not installed", "[Preset][Bundle][InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data_dir_scope(temp_dir.path());
|
||||||
|
RenameTestCollection coll;
|
||||||
|
|
||||||
|
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||||
|
parent.is_system = true;
|
||||||
|
|
||||||
|
// Fewer than one batch, so every file is read before the first one is committed.
|
||||||
|
constexpr int children = 8;
|
||||||
|
static_assert(children <= int(resolve_batch_size));
|
||||||
|
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||||
|
for (int i = 0; i < children; ++ i)
|
||||||
|
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
|
||||||
|
|
||||||
|
std::string first;
|
||||||
|
auto remove_the_others = [&](Preset &preset) {
|
||||||
|
if (! first.empty())
|
||||||
|
return;
|
||||||
|
first = preset.name;
|
||||||
|
for (int i = 0; i < children; ++ i)
|
||||||
|
if (const std::string name = "Child " + std::to_string(i); name != first)
|
||||||
|
fs::remove(dir / (name + ".json"));
|
||||||
|
};
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Disable, remove_the_others);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(first.empty());
|
||||||
|
CHECK(coll.error_count() == 0);
|
||||||
|
CHECK(coll.size() == 3); // the default preset, the parent and the first child
|
||||||
|
CHECK(coll.find_preset(first) != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Without the instance lock a user preset loads but its file is not written back", "[Preset][Bundle][InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data_dir_scope(temp_dir.path());
|
||||||
|
// A directory where the lock file belongs, so the lock cannot be taken.
|
||||||
|
fs::create_directories(fs::path(user_presets_lock_path()));
|
||||||
|
PresetBundle bundle;
|
||||||
|
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
|
||||||
|
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
|
||||||
|
const std::string before = read_file(file);
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
|
||||||
|
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
|
||||||
|
REQUIRE(preset != nullptr);
|
||||||
|
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
|
||||||
|
CHECK(read_file(file) == before);
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// File permissions stop reading only on POSIX.
|
||||||
|
TEST_CASE("A user preset that cannot be read under the instance lock is counted and removed", "[Preset][Bundle][InstanceLock]")
|
||||||
|
{
|
||||||
|
if (::geteuid() == 0)
|
||||||
|
SKIP("file permissions do not stop root");
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data_dir_scope(temp_dir.path());
|
||||||
|
RenameTestCollection coll;
|
||||||
|
|
||||||
|
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||||
|
parent.is_system = true;
|
||||||
|
const fs::path file = temp_dir.path() / PRESET_PRINT_NAME / "Unreadable.json";
|
||||||
|
write_minimal_child(file, "Unreadable", "Parent Process");
|
||||||
|
fs::permissions(file, fs::no_perms);
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Disable);
|
||||||
|
|
||||||
|
CHECK(coll.error_count() == 1);
|
||||||
|
CHECK(coll.find_preset("Unreadable") == nullptr);
|
||||||
|
CHECK_FALSE(fs::exists(file));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Creating a symlink needs no privilege only on POSIX.
|
||||||
|
TEST_CASE("A user preset that is a symlink to itself is counted and removed while the rest still load", "[Preset][Bundle][InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data_dir_scope(temp_dir.path());
|
||||||
|
RenameTestCollection coll;
|
||||||
|
|
||||||
|
Preset &parent = add_inmemory_preset(coll, "Parent Process");
|
||||||
|
parent.is_system = true;
|
||||||
|
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||||
|
write_minimal_child(dir / "Good A.json", "Good A", "Parent Process");
|
||||||
|
write_minimal_child(dir / "Good B.json", "Good B", "Parent Process");
|
||||||
|
const fs::path loop = dir / "Loop.json";
|
||||||
|
fs::create_symlink(loop.filename(), loop);
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
REQUIRE_NOTHROW(coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Disable));
|
||||||
|
|
||||||
|
CHECK(coll.error_count() == 1);
|
||||||
|
CHECK(coll.find_preset("Good A") != nullptr);
|
||||||
|
CHECK(coll.find_preset("Good B") != nullptr);
|
||||||
|
CHECK(coll.find_preset("Loop") == nullptr);
|
||||||
|
boost::system::error_code ec;
|
||||||
|
CHECK(fs::symlink_status(loop, ec).type() == fs::file_not_found);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// Creating a symlink needs no privilege only on POSIX.
|
||||||
|
TEST_CASE("A user preset symlinked to a missing target is counted but the link is kept", "[Preset][Bundle][InstanceLock]")
|
||||||
|
{
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
ScopedDataDir data_dir_scope(temp_dir.path());
|
||||||
|
RenameTestCollection coll;
|
||||||
|
|
||||||
|
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
|
||||||
|
const fs::path link = dir / "Linked.json";
|
||||||
|
fs::create_directories(dir);
|
||||||
|
fs::create_symlink(temp_dir.path() / "unmounted" / "Linked.json", link);
|
||||||
|
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::Disable);
|
||||||
|
|
||||||
|
CHECK(coll.error_count() == 1);
|
||||||
|
CHECK(coll.find_preset("Linked") == nullptr);
|
||||||
|
boost::system::error_code ec;
|
||||||
|
CHECK(fs::symlink_status(link, ec).type() == fs::symlink_file);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// The read-only bit on a directory stops file creation only on POSIX.
|
||||||
|
TEST_CASE("A user filament whose derived compatible printer cannot be written back still loads with the failure counted", "[Preset][Bundle]")
|
||||||
|
{
|
||||||
|
if (::geteuid() == 0)
|
||||||
|
SKIP("a read-only directory does not stop root");
|
||||||
|
ScopedTemporaryDir temp_dir;
|
||||||
|
PresetBundle bundle;
|
||||||
|
const fs::path dir = temp_dir.path() / PRESET_FILAMENT_NAME;
|
||||||
|
const fs::path file = dir / "My PLA @Test Printer.json";
|
||||||
|
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
|
||||||
|
const std::string before = read_file(file);
|
||||||
|
|
||||||
|
fs::permissions(file, fs::owner_read);
|
||||||
|
fs::permissions(dir, fs::owner_read | fs::owner_exe);
|
||||||
|
PresetsConfigSubstitutions substitutions;
|
||||||
|
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
|
||||||
|
fs::permissions(dir, fs::owner_all);
|
||||||
|
fs::permissions(file, fs::owner_read | fs::owner_write);
|
||||||
|
|
||||||
|
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
|
||||||
|
REQUIRE(preset != nullptr);
|
||||||
|
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
|
||||||
|
CHECK(bundle.filaments.error_count() == 1);
|
||||||
|
CHECK(read_file(file) == before);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|||||||
@@ -10,6 +10,8 @@
|
|||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
#ifndef _WIN32
|
#ifndef _WIN32
|
||||||
#include <unistd.h> // getuid
|
#include <unistd.h> // getuid
|
||||||
@@ -62,6 +64,113 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
|
|||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically replaces the target and leaves no temporary file", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "preset.json";
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), "second"));
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
load_string_file(target, content);
|
||||||
|
REQUIRE(content == "second");
|
||||||
|
size_t entries = 0;
|
||||||
|
for (auto &entry : boost::filesystem::directory_iterator(dir.path())) {
|
||||||
|
(void) entry;
|
||||||
|
++entries;
|
||||||
|
}
|
||||||
|
REQUIRE(entries == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically reports a missing directory and writes nothing", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "missing" / "preset.json";
|
||||||
|
|
||||||
|
const std::error_code ec = write_file_atomically(target.string(), "x");
|
||||||
|
REQUIRE(ec == std::errc::no_such_file_or_directory);
|
||||||
|
REQUIRE_FALSE(boost::filesystem::exists(target));
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
// The read-only bit on a directory stops file creation only on POSIX.
|
||||||
|
TEST_CASE("write_file_atomically writes in place when no temporary can be created beside an existing target", "[utils]") {
|
||||||
|
if (::geteuid() == 0)
|
||||||
|
SKIP("a read-only directory does not stop root");
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "preset.json";
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
|
||||||
|
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_read | boost::filesystem::owner_exe);
|
||||||
|
|
||||||
|
const std::error_code replaced = write_file_atomically(target.string(), "second");
|
||||||
|
const std::error_code created = write_file_atomically((dir.path() / "new.json").string(), "x");
|
||||||
|
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
|
||||||
|
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_all);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(replaced);
|
||||||
|
REQUIRE(created == std::errc::permission_denied);
|
||||||
|
std::string content;
|
||||||
|
load_string_file(target, content);
|
||||||
|
REQUIRE(content == "second");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "blob.bin";
|
||||||
|
const std::string bytes("a\r\nb\0c", 6);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(target.string(), bytes, /*binary=*/true));
|
||||||
|
REQUIRE(boost::filesystem::file_size(target) == bytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef _WIN32
|
||||||
|
TEST_CASE("write_file_atomically writes through a symlink and keeps the target's permissions", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path real = dir.path() / "real.json";
|
||||||
|
const boost::filesystem::path link = dir.path() / "link.json";
|
||||||
|
REQUIRE_FALSE(write_file_atomically(real.string(), "first"));
|
||||||
|
boost::filesystem::permissions(real, boost::filesystem::owner_read | boost::filesystem::owner_write);
|
||||||
|
boost::filesystem::create_symlink(real, link);
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(link.string(), "second"));
|
||||||
|
|
||||||
|
REQUIRE(boost::filesystem::is_symlink(boost::filesystem::symlink_status(link)));
|
||||||
|
std::string content;
|
||||||
|
load_string_file(real, content);
|
||||||
|
REQUIRE(content == "second");
|
||||||
|
|
||||||
|
REQUIRE_FALSE(write_file_atomically(real.string(), "third"));
|
||||||
|
const auto perms = boost::filesystem::status(real).permissions() & boost::filesystem::all_all;
|
||||||
|
REQUIRE(perms == (boost::filesystem::owner_read | boost::filesystem::owner_write));
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
TEST_CASE("write_file_atomically survives two threads writing one target", "[utils]") {
|
||||||
|
ScopedTemporaryDir dir;
|
||||||
|
const boost::filesystem::path target = dir.path() / "shared.json";
|
||||||
|
const std::string a(20000, 'a'), b(20000, 'b');
|
||||||
|
|
||||||
|
std::thread other([&] {
|
||||||
|
for (int i = 0; i < 50; ++i)
|
||||||
|
write_file_atomically(target.string(), a);
|
||||||
|
});
|
||||||
|
for (int i = 0; i < 50; ++i)
|
||||||
|
write_file_atomically(target.string(), b);
|
||||||
|
other.join();
|
||||||
|
|
||||||
|
std::string content;
|
||||||
|
load_string_file(target, content);
|
||||||
|
const bool whole = content == a || content == b;
|
||||||
|
REQUIRE(whole);
|
||||||
|
// No temporary may be left; a scanner on Windows may briefly hold the old
|
||||||
|
// file under another name, so only the temporaries are counted.
|
||||||
|
size_t temporaries = 0;
|
||||||
|
for (auto &entry : boost::filesystem::directory_iterator(dir.path()))
|
||||||
|
if (entry.path().extension() == ".tmp")
|
||||||
|
++temporaries;
|
||||||
|
REQUIRE(temporaries == 0);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
|
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
|
||||||
ScopedTemporaryFile source(".txt");
|
ScopedTemporaryFile source(".txt");
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -3,12 +3,21 @@
|
|||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/crc.hpp>
|
#include <boost/crc.hpp>
|
||||||
#include <cereal/archives/binary.hpp>
|
#include <cereal/archives/binary.hpp>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <atomic>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <memory>
|
||||||
|
#include <numeric>
|
||||||
|
#include <random>
|
||||||
#include <set>
|
#include <set>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
#include <tbb/blocked_range.h>
|
||||||
|
#include <tbb/parallel_for.h>
|
||||||
|
#include <tbb/task_group.h>
|
||||||
|
|
||||||
#include "libslic3r/PresetBundle.hpp"
|
#include "libslic3r/PresetBundle.hpp"
|
||||||
#include "libslic3r/PresetCacheFormat.hpp"
|
#include "libslic3r/PresetCacheFormat.hpp"
|
||||||
#include "libslic3r/Preset.hpp"
|
#include "libslic3r/Preset.hpp"
|
||||||
@@ -52,6 +61,27 @@ void write_vendor_tree(const fs::path& dir, const std::string& vendor, const std
|
|||||||
<< R"(","from":"system","instantiation":"true","layer_height":"0.2"})";
|
<< R"(","from":"system","instantiation":"true","layer_height":"0.2"})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A vendor whose process list is `processes`, each a preset name and the text of
|
||||||
|
// its sub-file, listed in that order.
|
||||||
|
void write_process_vendor(const fs::path& dir, const std::string& vendor,
|
||||||
|
const std::vector<std::pair<std::string, std::string>>& processes)
|
||||||
|
{
|
||||||
|
fs::create_directories(dir / vendor / "process");
|
||||||
|
std::ofstream index((dir / (vendor + ".json")).string());
|
||||||
|
index << R"({"version":"1.0.0","name":")" << vendor << R"(","process_list":[)";
|
||||||
|
for (size_t i = 0; i < processes.size(); ++ i) {
|
||||||
|
const std::string sub_path = "process/p" + std::to_string(i) + ".json";
|
||||||
|
index << (i ? "," : "") << R"({"name":")" << processes[i].first << R"(","sub_path":")" << sub_path << R"("})";
|
||||||
|
std::ofstream((dir / vendor / sub_path).string()) << processes[i].second;
|
||||||
|
}
|
||||||
|
index << "]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string process_json(const std::string& name, const std::string& extra = std::string())
|
||||||
|
{
|
||||||
|
return R"({"type":"process","name":")" + name + R"(","from":"system",)" + extra + R"("layer_height":"0.2"})";
|
||||||
|
}
|
||||||
|
|
||||||
// A small but complete vendor: one machine model, one process, a non-instantiated
|
// A small but complete vendor: one machine model, one process, a non-instantiated
|
||||||
// base filament with an instantiated child that inherits it and includes a
|
// base filament with an instantiated child that inherits it and includes a
|
||||||
// dual-extruder template, a second standalone filament carrying explicit
|
// dual-extruder template, a second standalone filament carrying explicit
|
||||||
@@ -128,6 +158,80 @@ void write_lib_tree(const fs::path& dir, const std::string& version, const std::
|
|||||||
<< R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})";
|
<< R"("filament_id":"GFL99","filament_cost":")" << cost << R"("})";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct FixtureEntry {
|
||||||
|
std::string name, sub_path;
|
||||||
|
std::vector<size_t> deps; // indices of the entries this one inherits or includes
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filaments with two levels of inheritance, a template to include, and `leaves`
|
||||||
|
// presets that inherit either level, some including the template. Only the
|
||||||
|
// subfiles are written; write_filament_list lists them.
|
||||||
|
std::vector<FixtureEntry> write_layered_filaments(const fs::path& dir, const std::string& vendor, int leaves)
|
||||||
|
{
|
||||||
|
fs::create_directories(dir / vendor / "filament");
|
||||||
|
std::vector<FixtureEntry> entries {
|
||||||
|
{ vendor + " Base PLA", "filament/base.json", {} },
|
||||||
|
{ vendor + " Mid PLA", "filament/mid.json", {0} },
|
||||||
|
{ vendor + " dual template", "filament/template.json", {} },
|
||||||
|
};
|
||||||
|
std::ofstream((dir / vendor / "filament" / "base.json").string())
|
||||||
|
<< R"({"type":"filament","name":")" << entries[0].name << R"(","from":"system","instantiation":"false",)"
|
||||||
|
<< R"("filament_id":"GFA_base","filament_cost":"42","filament_max_volumetric_speed":["12"]})";
|
||||||
|
std::ofstream((dir / vendor / "filament" / "mid.json").string())
|
||||||
|
<< R"({"type":"filament","name":")" << entries[1].name << R"(","from":"system","instantiation":"false",)"
|
||||||
|
<< R"("inherits":")" << entries[0].name << R"(","filament_flow_ratio":"0.95"})";
|
||||||
|
std::ofstream((dir / vendor / "filament" / "template.json").string())
|
||||||
|
<< R"({"type":"filament","name":")" << entries[2].name << R"(","from":"system","instantiation":"false",)"
|
||||||
|
<< R"("filament_extruder_variant":["Direct Drive Standard","Direct Drive High Flow"],)"
|
||||||
|
<< R"("filament_max_volumetric_speed":["20","22"]})";
|
||||||
|
for (int i = 0; i < leaves; ++ i) {
|
||||||
|
const size_t parent = i % 2;
|
||||||
|
const bool include = i % 3 == 0;
|
||||||
|
entries.push_back({ vendor + " PLA " + std::to_string(i) + " @0.4", "filament/leaf" + std::to_string(i) + ".json",
|
||||||
|
include ? std::vector<size_t>{parent, 2} : std::vector<size_t>{parent} });
|
||||||
|
std::ofstream((dir / vendor / entries.back().sub_path).string())
|
||||||
|
<< R"({"type":"filament","name":")" << entries.back().name << R"(","from":"system","instantiation":"true",)"
|
||||||
|
<< R"("inherits":")" << entries[parent].name << R"(",)"
|
||||||
|
<< (include ? R"("include":[")" + entries[2].name + R"("],)" : std::string())
|
||||||
|
<< R"("nozzle_temperature":[")" << (200 + i % 40) << R"("]})";
|
||||||
|
}
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The vendor profile for write_layered_filaments, listing its entries in `order`,
|
||||||
|
// or as they were written when `order` is empty.
|
||||||
|
void write_filament_list(const fs::path& dir, const std::string& vendor, const std::vector<FixtureEntry>& entries,
|
||||||
|
std::vector<size_t> order = {})
|
||||||
|
{
|
||||||
|
if (order.empty()) {
|
||||||
|
order.resize(entries.size());
|
||||||
|
std::iota(order.begin(), order.end(), 0);
|
||||||
|
}
|
||||||
|
std::ofstream f((dir / (vendor + ".json")).string());
|
||||||
|
f << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)";
|
||||||
|
for (size_t k = 0; k < order.size(); ++ k)
|
||||||
|
f << (k ? "," : "") << R"({"name":")" << entries[order[k]].name << R"(","sub_path":")"
|
||||||
|
<< entries[order[k]].sub_path << R"("})";
|
||||||
|
f << "]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
// A random order of `entries` that lists each one after everything it depends on.
|
||||||
|
std::vector<size_t> shuffled_after_dependencies(const std::vector<FixtureEntry>& entries, std::mt19937& rng)
|
||||||
|
{
|
||||||
|
std::vector<size_t> order;
|
||||||
|
std::vector<bool> listed(entries.size(), false);
|
||||||
|
while (order.size() < entries.size()) {
|
||||||
|
std::vector<size_t> ready;
|
||||||
|
for (size_t i = 0; i < entries.size(); ++ i)
|
||||||
|
if (! listed[i] && std::all_of(entries[i].deps.begin(), entries[i].deps.end(), [&](size_t d) { return listed[d]; }))
|
||||||
|
ready.push_back(i);
|
||||||
|
const size_t next = ready[std::uniform_int_distribution<size_t>(0, ready.size() - 1)(rng)];
|
||||||
|
listed[next] = true;
|
||||||
|
order.push_back(next);
|
||||||
|
}
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
|
||||||
// A vendor whose one filament inherits the library's base and states nothing of
|
// A vendor whose one filament inherits the library's base and states nothing of
|
||||||
// its own — everything it shows comes from the library it is resolved against.
|
// its own — everything it shows comes from the library it is resolved against.
|
||||||
void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version)
|
void write_vendor_with_lib_filament(const fs::path& dir, const std::string& vendor, const std::string& version)
|
||||||
@@ -624,6 +728,298 @@ TEST_CASE("a cache-loaded vendor is indistinguishable from a JSON-loaded one", "
|
|||||||
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // through the include
|
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template"); // through the include
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a wide vendor loads from its cache exactly as it loads from JSON", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
constexpr int leaves = 400;
|
||||||
|
write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", leaves));
|
||||||
|
|
||||||
|
PresetBundle from_json;
|
||||||
|
from_json.set_generate_vendor_caches(true);
|
||||||
|
from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
|
||||||
|
|
||||||
|
fs::remove_all(dirs.system / "Acme");
|
||||||
|
PresetBundle from_cache;
|
||||||
|
from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
|
||||||
|
auto a = presets_for(from_json.filaments, "Acme");
|
||||||
|
auto b = presets_for(from_cache.filaments, "Acme");
|
||||||
|
REQUIRE(a.size() == size_t(leaves));
|
||||||
|
REQUIRE(b.size() == a.size());
|
||||||
|
for (size_t i = 0; i < a.size(); ++ i) {
|
||||||
|
CHECK(a[i]->name == b[i]->name);
|
||||||
|
CHECK(preset_deep_equal(*a[i], *b[i]));
|
||||||
|
}
|
||||||
|
CHECK(from_cache.error_count() == from_json.error_count());
|
||||||
|
// The two-level chain resolved, so the leaf has the middle profile's flow ratio
|
||||||
|
// and the base profile's filament id, neither of which it states.
|
||||||
|
const Preset* leaf = from_cache.filaments.find_preset("Acme PLA 7 @0.4", false);
|
||||||
|
REQUIRE(leaf != nullptr);
|
||||||
|
CHECK(leaf->filament_id == "GFA_base");
|
||||||
|
const auto* flow = leaf->config.option<ConfigOptionFloats>("filament_flow_ratio");
|
||||||
|
REQUIRE(flow != nullptr);
|
||||||
|
CHECK_THAT(flow->values.front(), WithinAbs(0.95, 1e-9));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("presets install the same in any order that lists each after what it depends on", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
const std::vector<FixtureEntry> entries = write_layered_filaments(dirs.system, "Acme", 150);
|
||||||
|
auto load = [&](const std::vector<size_t>& order) {
|
||||||
|
write_filament_list(dirs.system, "Acme", entries, order);
|
||||||
|
auto bundle = std::make_unique<PresetBundle>();
|
||||||
|
bundle->load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
return bundle;
|
||||||
|
};
|
||||||
|
const auto reference = load({});
|
||||||
|
CHECK(reference->error_count() == 0);
|
||||||
|
const auto expected = presets_for(reference->filaments, "Acme");
|
||||||
|
REQUIRE(expected.size() == 150);
|
||||||
|
|
||||||
|
// Every seed checks against one reference load, which GENERATE would repeat per seed.
|
||||||
|
for (unsigned seed = 0; seed < 8; ++ seed) {
|
||||||
|
CAPTURE(seed);
|
||||||
|
std::mt19937 rng(seed);
|
||||||
|
const auto shuffled = load(shuffled_after_dependencies(entries, rng));
|
||||||
|
CHECK(shuffled->error_count() == 0);
|
||||||
|
const auto actual = presets_for(shuffled->filaments, "Acme");
|
||||||
|
REQUIRE(actual.size() == expected.size());
|
||||||
|
for (size_t i = 0; i < actual.size(); ++ i)
|
||||||
|
CHECK(preset_deep_equal(*actual[i], *expected[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a key misplaced into a vendor preset is reported and removed", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_process_vendor(dirs.system, "Acme", {
|
||||||
|
{ "Acme template", R"({"type":"process","name":"Acme template","from":"system","instantiation":"false","wall_loops":"5"})" },
|
||||||
|
{ "Acme base", R"({"type":"process","name":"Acme base","from":"system","instantiation":"false","filament_cost":"5"})" },
|
||||||
|
{ "0.20mm Standard @Acme", process_json("0.20mm Standard @Acme",
|
||||||
|
R"("instantiation":"true","inherits":"Acme base","include":["Acme template"],"nozzle_temperature":["210"],)") } });
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
|
||||||
|
CHECK(bundle.error_count() == 2);
|
||||||
|
const Preset* preset = bundle.prints.find_preset("0.20mm Standard @Acme", false);
|
||||||
|
REQUIRE(preset != nullptr);
|
||||||
|
CHECK_FALSE(preset->config.has("filament_cost"));
|
||||||
|
CHECK_FALSE(preset->config.has("nozzle_temperature"));
|
||||||
|
CHECK(preset->config.opt_int("wall_loops") == 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("each vendor loads from the directory it is listed with", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_vendor_tree(dirs.system, "Acme", "1.0.0");
|
||||||
|
write_vendor_tree(dirs.profiles, "Zeta", "2.0.0");
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.profiles } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false);
|
||||||
|
|
||||||
|
CHECK(bundle.vendors.count("Acme") == 1);
|
||||||
|
CHECK(bundle.vendors.count("Zeta") == 1);
|
||||||
|
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
|
||||||
|
CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a vendor that fails to load is left out, reported, and the others still load", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_vendor_tree(dirs.system, "Acme", "1.0.0");
|
||||||
|
write_process_vendor(dirs.system, "Broken", { { "Broken A", "{not-json" } });
|
||||||
|
PresetBundle bundle;
|
||||||
|
std::vector<std::string> failed;
|
||||||
|
const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||||
|
/*allow_cache=*/false, nullptr, &failed).second;
|
||||||
|
|
||||||
|
CHECK(bundle.vendors.count("Acme") == 1);
|
||||||
|
CHECK(bundle.vendors.count("Broken") == 0);
|
||||||
|
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
|
||||||
|
CHECK(errors.find("Broken") != std::string::npos);
|
||||||
|
CHECK(failed == std::vector<std::string>{ "Broken" });
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a filament library that fails partway is reported, and vendors inheriting from it are left out", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||||
|
write_lib_tree(dirs.system, "1.0.0", "20");
|
||||||
|
std::ofstream((dirs.system / (lib + ".json")).string())
|
||||||
|
<< R"({"version":"1.0.0","name":")" << lib << R"(","filament_list":[)"
|
||||||
|
<< R"({"name":"Generic PLA","sub_path":"filament/generic_pla.json"},)"
|
||||||
|
<< R"({"name":"Generic PETG","sub_path":"filament/generic_petg.json"}]})";
|
||||||
|
std::ofstream((dirs.system / lib / "filament" / "generic_petg.json").string()) << "{not-json";
|
||||||
|
write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0");
|
||||||
|
write_vendor_tree(dirs.system, "Zeta", "1.0.0");
|
||||||
|
|
||||||
|
PresetBundle bundle;
|
||||||
|
std::vector<std::string> failed;
|
||||||
|
const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { lib, dirs.system }, { "Zeta", dirs.system } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||||
|
/*allow_cache=*/false, nullptr, &failed).second;
|
||||||
|
|
||||||
|
CHECK(failed == std::vector<std::string>{ lib, "Acme" });
|
||||||
|
CHECK(errors.find("generic_petg.json") != std::string::npos);
|
||||||
|
CHECK(bundle.vendors.count(lib) == 1);
|
||||||
|
CHECK(bundle.vendors.count("Acme") == 0);
|
||||||
|
CHECK(bundle.filaments.find_preset("Acme PLA @0.4", false) == nullptr);
|
||||||
|
CHECK(bundle.vendors.count("Zeta") == 1);
|
||||||
|
CHECK(bundle.prints.find_preset("0.20mm Standard @Zeta", false) != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a vendor whose profile cannot be read is left out, reported, and the others still load", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_vendor_tree(dirs.system, "Acme", "1.0.0");
|
||||||
|
std::ofstream((dirs.system / "Broken.json").string()) << "{not-json";
|
||||||
|
PresetBundle bundle;
|
||||||
|
const std::string errors = bundle.load_vendors({ { "Acme", dirs.system }, { "Broken", dirs.system } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||||
|
/*allow_cache=*/false).second;
|
||||||
|
|
||||||
|
CHECK(bundle.vendors.count("Acme") == 1);
|
||||||
|
CHECK(bundle.vendors.count("Broken") == 0);
|
||||||
|
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
|
||||||
|
CHECK(errors.find("Broken.json") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a canceled vendor load starts no vendor", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_vendor_tree(dirs.system, "Acme", "1.0.0");
|
||||||
|
write_vendor_tree(dirs.system, "Zeta", "1.0.0");
|
||||||
|
const std::atomic<bool> cancel { true };
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.load_vendors({ { "Acme", dirs.system }, { "Zeta", dirs.system } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false, &cancel);
|
||||||
|
|
||||||
|
CHECK(bundle.vendors.empty());
|
||||||
|
CHECK(bundle.prints.find_preset("0.20mm Standard @Acme", false) == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a preset two vendors both define is kept from the first listed and reported under the others", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
for (const std::string vendor : { "Acme", "Mira", "Zeta" })
|
||||||
|
write_process_vendor(dirs.system, vendor, {
|
||||||
|
{ "Shared", process_json("Shared", R"("instantiation":"true",)") },
|
||||||
|
{ "Own @" + vendor, process_json("Own @" + vendor, R"("instantiation":"true",)") } });
|
||||||
|
PresetBundle bundle;
|
||||||
|
const std::string errors = bundle.load_vendors({ { "Mira", dirs.system }, { "Acme", dirs.system }, { "Zeta", dirs.system } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent,
|
||||||
|
/*allow_cache=*/false).second;
|
||||||
|
|
||||||
|
const Preset* shared = bundle.prints.find_preset("Shared", false);
|
||||||
|
REQUIRE(shared != nullptr);
|
||||||
|
REQUIRE(shared->vendor != nullptr);
|
||||||
|
CHECK(shared->vendor->id == "Mira");
|
||||||
|
CHECK(errors.find("vendor Mira") == std::string::npos);
|
||||||
|
CHECK(errors.find("Found duplicated settings in vendor Acme's json file lists: Shared") != std::string::npos);
|
||||||
|
CHECK(errors.find("Found duplicated settings in vendor Zeta's json file lists: Shared") != std::string::npos);
|
||||||
|
CHECK(bundle.error_count() == 2);
|
||||||
|
for (const std::string vendor : { "Acme", "Mira", "Zeta" }) {
|
||||||
|
const Preset* own = bundle.prints.find_preset("Own @" + vendor, false);
|
||||||
|
REQUIRE(own != nullptr);
|
||||||
|
CHECK(own->vendor == &bundle.vendors.at(vendor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("filaments merged from several vendors come out generic first, then by name", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
auto write_filaments = [&](const std::string& vendor, const std::vector<std::string>& names) {
|
||||||
|
fs::create_directories(dirs.system / vendor / "filament");
|
||||||
|
std::ofstream index((dirs.system / (vendor + ".json")).string());
|
||||||
|
index << R"({"version":"1.0.0","name":")" << vendor << R"(","filament_list":[)";
|
||||||
|
for (size_t i = 0; i < names.size(); ++ i) {
|
||||||
|
const std::string sub_path = "filament/f" + std::to_string(i) + ".json";
|
||||||
|
index << (i ? "," : "") << R"({"name":")" << names[i] << R"(","sub_path":")" << sub_path << R"("})";
|
||||||
|
std::ofstream((dirs.system / vendor / sub_path).string())
|
||||||
|
<< R"({"type":"filament","name":")" << names[i] << R"(","from":"system","instantiation":"true",)"
|
||||||
|
<< R"("filament_id":"GF)" << vendor << i << R"("})";
|
||||||
|
}
|
||||||
|
index << "]}";
|
||||||
|
};
|
||||||
|
write_filaments("Zeta", { "Zeta PLA @0.4", "Generic PETG @Zeta" });
|
||||||
|
write_filaments("Acme", { "Acme PLA @0.4", "Generic PLA @Acme" });
|
||||||
|
PresetBundle bundle;
|
||||||
|
bundle.load_vendors({ { "Zeta", dirs.system }, { "Acme", dirs.system } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/false);
|
||||||
|
|
||||||
|
std::vector<std::string> names;
|
||||||
|
for (const Preset& preset : bundle.filaments.get_presets())
|
||||||
|
if (! preset.is_default)
|
||||||
|
names.push_back(preset.name);
|
||||||
|
CHECK(names == std::vector<std::string>{ "Generic PETG @Zeta", "Generic PLA @Acme", "Acme PLA @0.4", "Zeta PLA @0.4" });
|
||||||
|
for (const std::string& name : names)
|
||||||
|
CHECK(bundle.filaments.find_preset(name, false) != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a vendor read while the filament library loads resolves against it, from JSON and from its cache", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||||
|
write_lib_tree(dirs.system, "1.0.0", "20");
|
||||||
|
write_vendor_with_lib_filament(dirs.system, "Acme", "1.0.0");
|
||||||
|
auto load = [&] {
|
||||||
|
auto bundle = std::make_unique<PresetBundle>();
|
||||||
|
bundle->set_generate_vendor_caches(true);
|
||||||
|
bundle->load_vendors({ { "Acme", dirs.system }, { lib, dirs.system } },
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true);
|
||||||
|
return bundle;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto from_json = load();
|
||||||
|
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
|
||||||
|
fs::remove_all(dirs.system / "Acme");
|
||||||
|
const auto from_cache = load();
|
||||||
|
|
||||||
|
for (const PresetBundle* bundle : { from_json.get(), from_cache.get() }) {
|
||||||
|
const Preset* pla = bundle->filaments.find_preset("Acme PLA @0.4", false);
|
||||||
|
REQUIRE(pla != nullptr);
|
||||||
|
CHECK(pla->filament_id == "GFL99");
|
||||||
|
CHECK_THAT(pla->config.option<ConfigOptionFloats>("filament_cost")->values.front(), WithinAbs(20., 1e-9));
|
||||||
|
CHECK(bundle->error_count() == 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("repeated cache loads of one vendor produce the same presets", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_filament_list(dirs.system, "Acme", write_layered_filaments(dirs.system, "Acme", 400));
|
||||||
|
|
||||||
|
PresetBundle seed;
|
||||||
|
seed.set_generate_vendor_caches(true);
|
||||||
|
seed.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
|
||||||
|
fs::remove_all(dirs.system / "Acme");
|
||||||
|
|
||||||
|
std::vector<PresetBundle> loads(3);
|
||||||
|
for (PresetBundle& bundle : loads)
|
||||||
|
bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
|
||||||
|
auto first = presets_for(loads[0].filaments, "Acme");
|
||||||
|
REQUIRE(!first.empty());
|
||||||
|
for (size_t run = 1; run < loads.size(); ++ run) {
|
||||||
|
auto again = presets_for(loads[run].filaments, "Acme");
|
||||||
|
REQUIRE(again.size() == first.size());
|
||||||
|
for (size_t i = 0; i < first.size(); ++ i) {
|
||||||
|
CHECK(first[i]->name == again[i]->name);
|
||||||
|
CHECK(preset_deep_equal(*first[i], *again[i]));
|
||||||
|
}
|
||||||
|
CHECK(loads[run].error_count() == loads[0].error_count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]")
|
TEST_CASE("a cache-served vendor reports the errors its parse counted", "[VendorCache]")
|
||||||
{
|
{
|
||||||
TempDir tmp;
|
TempDir tmp;
|
||||||
@@ -866,6 +1262,27 @@ TEST_CASE("a vendor installed as its cache alone still loads after a library upd
|
|||||||
CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9));
|
CHECK_THAT(cost->values.front(), WithinAbs(30., 1e-9));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a cache install stopped by a canceled task group throws instead of rejecting the cache", "[VendorCache]")
|
||||||
|
{
|
||||||
|
TempDir tmp;
|
||||||
|
const std::string cache = (tmp.path / "Acme.opc").string();
|
||||||
|
REQUIRE(save_one_vendor(cache, one_vendor("Acme"), "Acme", "1.0.0", {filament_entry("Acme PLA @0.4")}));
|
||||||
|
|
||||||
|
PresetBundle bundle;
|
||||||
|
bool threw = false;
|
||||||
|
tbb::task_group_context context;
|
||||||
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, 1), [&](const tbb::blocked_range<size_t>&) {
|
||||||
|
context.cancel_group_execution();
|
||||||
|
try {
|
||||||
|
bundle.load_vendor_cache(cache, "Acme", Semver(1, 0, 0));
|
||||||
|
} catch (const std::runtime_error&) {
|
||||||
|
threw = true;
|
||||||
|
}
|
||||||
|
}, context);
|
||||||
|
|
||||||
|
CHECK(threw);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]")
|
TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSONs", "[VendorCache]")
|
||||||
{
|
{
|
||||||
TempDir tmp;
|
TempDir tmp;
|
||||||
@@ -893,6 +1310,12 @@ TEST_CASE("a cache entry whose parent is missing falls back to the vendor's JSON
|
|||||||
user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
user.string(), "Acme", PresetBundle::LoadSystem, ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
CHECK(presets_loaded == 1);
|
CHECK(presets_loaded == 1);
|
||||||
CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's
|
CHECK(out.vendors.at("Acme").name == "Acme"); // the profile's name, not the cache's
|
||||||
|
|
||||||
|
// Through load_vendors, which reads the cache before installing it.
|
||||||
|
PresetBundle several;
|
||||||
|
several.load_vendors({ { "Acme", user } }, ForwardCompatibilitySubstitutionRule::EnableSilent, /*allow_cache=*/true);
|
||||||
|
CHECK(several.vendors.at("Acme").name == "Acme");
|
||||||
|
CHECK(several.prints.find_preset("0.20mm Standard @Acme", false) != nullptr);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]")
|
TEST_CASE("a profile with no usable version is never served from cache", "[VendorCache]")
|
||||||
@@ -1380,24 +1803,6 @@ TEST_CASE("a header claiming more body than the file holds is rejected", "[Vendo
|
|||||||
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
|
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]")
|
|
||||||
{
|
|
||||||
TempDir tmp;
|
|
||||||
const std::string cache = (tmp.path / "Durable.opc").string();
|
|
||||||
REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0"));
|
|
||||||
const std::string before = slurp(cache);
|
|
||||||
|
|
||||||
// A directory where the temp file wants to go: the write cannot complete,
|
|
||||||
// and must not have destroyed what was already there to find that out.
|
|
||||||
const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp");
|
|
||||||
fs::create_directories(blocker);
|
|
||||||
|
|
||||||
REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0"));
|
|
||||||
CHECK(slurp(cache) == before);
|
|
||||||
|
|
||||||
fs::remove_all(blocker);
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
|
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
|
||||||
{
|
{
|
||||||
// The regression the fingerprint used to prevent by refusing the file
|
// The regression the fingerprint used to prevent by refusing the file
|
||||||
@@ -1691,6 +2096,31 @@ TEST_CASE("an include listed after the preset that names it is an error, and the
|
|||||||
CHECK(silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>{12.});
|
CHECK(silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values == std::vector<double>{12.});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("an include listed after the preset that names it is missing from the cache load too", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_full_vendor_tree(dirs.system, "Acme", "1.0.0", /*templates_last=*/true);
|
||||||
|
PresetBundle from_json;
|
||||||
|
from_json.set_generate_vendor_caches(true);
|
||||||
|
from_json.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
REQUIRE(fs::exists(dirs.system / "Acme.opc"));
|
||||||
|
|
||||||
|
fs::remove_all(dirs.system / "Acme");
|
||||||
|
PresetBundle from_cache;
|
||||||
|
from_cache.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||||
|
CHECK(from_cache.error_count() == 2);
|
||||||
|
const Preset* pr = from_cache.printers.find_preset("Acme 0.4 nozzle", false);
|
||||||
|
REQUIRE(pr != nullptr);
|
||||||
|
CHECK(pr->config.opt_string("machine_start_gcode") != "G28 ; template");
|
||||||
|
const Preset* silk = from_cache.filaments.find_preset("Acme Silk PLA @0.4", false);
|
||||||
|
REQUIRE(silk != nullptr);
|
||||||
|
const auto& speed = silk->config.option<ConfigOptionFloats>("filament_max_volumetric_speed")->values;
|
||||||
|
REQUIRE(speed.size() == 1);
|
||||||
|
CHECK_THAT(speed.front(), WithinAbs(12., 1e-9));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]")
|
TEST_CASE("a G-code template that states no instantiation is included, not loaded as a preset", "[VendorCache]")
|
||||||
{
|
{
|
||||||
InstallDirs dirs;
|
InstallDirs dirs;
|
||||||
@@ -1712,3 +2142,35 @@ TEST_CASE("a G-code template that states no instantiation is included, not loade
|
|||||||
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template");
|
CHECK(pr->config.opt_string("machine_start_gcode") == "G28 ; template");
|
||||||
CHECK(presets_for(bundle.printers, "Acme").size() == 1);
|
CHECK(presets_for(bundle.printers, "Acme").size() == 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a sub-file that fails to parse leaves the ones listed before it installed", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
write_process_vendor(dirs.system, "Acme", {
|
||||||
|
{ "Acme A", process_json("Acme A", R"("instantiation":"true",)") },
|
||||||
|
{ "Acme B", "{not-json" },
|
||||||
|
{ "Acme C", process_json("Acme C", R"("instantiation":"true",)") } });
|
||||||
|
PresetBundle bundle;
|
||||||
|
CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent),
|
||||||
|
ConfigurationError);
|
||||||
|
CHECK(bundle.prints.find_preset("Acme A", false) != nullptr);
|
||||||
|
CHECK(bundle.prints.find_preset("Acme C", false) == nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("a load that fails to install partway counts no errors of the sub-files after it", "[VendorCache]")
|
||||||
|
{
|
||||||
|
InstallDirs dirs;
|
||||||
|
// B names a parent nobody defines. C states no instantiation, an error of its own.
|
||||||
|
write_process_vendor(dirs.system, "Acme", {
|
||||||
|
{ "Acme A", process_json("Acme A", R"("instantiation":"true",)") },
|
||||||
|
{ "Acme B", process_json("Acme B", R"("instantiation":"true","inherits":"Nobody",)") },
|
||||||
|
{ "Acme C", process_json("Acme C") } });
|
||||||
|
PresetBundle bundle;
|
||||||
|
CHECK_THROWS_AS(bundle.load_vendor_configs_from_json(dirs.system.string(), "Acme", PresetBundle::LoadSystem,
|
||||||
|
ForwardCompatibilitySubstitutionRule::EnableSilent),
|
||||||
|
ConfigurationError);
|
||||||
|
// B's missing parent and B's failed install.
|
||||||
|
CHECK(bundle.error_count() == 2);
|
||||||
|
CHECK(bundle.prints.find_preset("Acme A", false) != nullptr);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user