Compare commits

..
Author SHA1 Message Date
Hanif Koh 35dc1ca6ef Lock Config and Preset Files Across Instances and Write Them Atomically
Every running instance shares one OrcaSlicer.conf and one user preset
tree, and nothing kept their writers apart. Two instances saving at the
same moment, or the cloud preset sync thread writing while the GUI thread
saved, could interleave, and a reader in another instance could open a
preset JSON or .info file between truncate and close and get a partial
file, dropping that preset for the session with a parse error.

Add InstanceLock, a scoped guard that serialises the threads of one
process through a recursive mutex and other processes through an advisory
OS file lock: flock on POSIX, held on the guard's own descriptor so no
other close in the process can drop it, and LockFileEx on Windows. The
outermost guard opens the lock file and closes it on release, so nothing
stays open between saves and a data dir can be removed once nothing is
saving into it; the file itself is kept, since deleting it would let a
third instance lock a fresh file while the second still holds the old
one. It is best effort: when the lock file cannot be opened or locked, or
another instance still holds it after a second, the guard logs once and
lets the write proceed, then leaves the file alone for ten seconds, so a
hung instance never blocks every other one and a holder stuck in a
debugger does not cost a stall per save. The guard sits at the leaf
readers and writers: set_sync_info_and_save() calls save_info() under the
preset collection mutex, so a batch lock around save_user_presets() would
invert the order against the sync thread. The user preset scan reads its
files on worker threads without the guard, since the mutex would
serialise them, and takes it per file in the serial commit step, so a
save never waits for the whole scan. Each read keeps the bytes of the
preset and its .info as they were before parsing; commit compares them
with the disk under the guard and reads a file that changed again, so it
never deletes or writes back over another instance's newer save, nor
installs a .json and .info from two different saves; a preset another
instance removed in the meantime is not installed. Without the guard, in
a cool-down, the scan still loads the presets but leaves their files
alone: an unreadable file stays for the next scan, and a derived
compatible printer is not written back. Read-only scans, which is what
the CLI does, take no lock and create no lock file.

AppConfig holds OrcaSlicer.conf.lock in load() and save(); load is
included because the Windows path restores from the .bak copy. Every
user preset writer and reader holds user.lock: Preset::save(), which
writes no .info when the preset itself could not be written, since an
.info without its preset reads as a cloud deletion request, save_info(),
reload() and remove_files(), each preset the scan commits, the
bundle metadata reads and write, the .info removal after a cloud-confirmed
delete, the orphaned-.info scan on the sync thread, the bundle folder
removal on unsubscribe and the physical printer writers and delete
paths. A bundle import extracts under cache/ into a folder per process
and per import, where no scan reads.

Preset JSON, .info, bundle metadata, physical printer and config files,
and the caches and state files that already used a temporary by hand,
now go through write_file_atomically(), which writes <file>.<pid>.<n>.tmp
beside the target and renames it over, so a reader that never waits sees
a complete old or new file. A symlink is followed; a target that is not
a regular file is written in place; and when no temporary can be created
beside an existing target, or the rename itself is refused, by a Windows
reader holding the file open or a mount that cannot replace in one step,
the helper writes in place as before, since losing the save is worse
than a torn read. On POSIX the rename replaces the
target atomically where the old code removed it first and left a window
with no file at all; only a mount that refuses a one-step replace gets
the old remove-then-rename. A crash between temporary and rename leaves
the temporary behind, which no scan reads. Preset::save() returns
whether it wrote the preset, so the scan counts a compatible printer it
could not write back as an error. A failed config write keeps
the config dirty, and the idle handler waits ten seconds before retrying
while an explicit save always tries.
2026-09-28 16:21:31 +08:00
raistlin7447 7f30eb8a49 libslic3r: merge the loaded vendors' presets in one pass
load_vendors merged each vendor's bundle into the startup bundle in turn,
inserting every preset at its sorted place in a deque, so each insert shifted
every preset after it.

PresetBundle::merge_presets now takes every loaded bundle at once, and each
collection merges its sorted presets with theirs in one pass. A preset name this
bundle or an earlier one of the list already has is still left out and reported
under the vendor that repeats it, in the same order as before.
2026-09-27 08:42:38 -05:00
raistlin7447 b5d1866445 libslic3r: read the other vendors while the filament library loads
Installing a vendor's filaments looks up the filament library, so load_vendors
loads the library first. Reading a vendor, from its cache or its JSONs, needs
nothing from the library, yet every other vendor waited for it before starting.

load_vendor_configs_from_json splits into read_vendor and install_vendor_read.
load_vendors reads the other vendors in parallel while the library loads, and
installs each against it as soon as both its read and the library are done. A
cache the library can no longer install is still replaced by a parse of the
vendor's JSONs, now when it installs; a canceled load is not mistaken for such
a cache. Vendors start costliest first, and a cache older than the profile
beside it is ordered as a parse.

load_vendors also names the vendors that failed, the library included, which
keeps what it installed before failing and so stays among the bundle's vendors.
The setup wizard falls back to its own scan on that list rather than on which
vendors the bundle holds.
2026-09-27 08:42:37 -05:00
raistlin7447 3b1a5c0630 libslic3r: copy the configs a vendor keeps while resolving
A vendor keeps a copy of each config its other entries inherit, and the filament
library keeps every one of them for the vendors that inherit from it. The copy
was made in commit, one entry at a time on the calling thread while the workers
waited, and the library alone makes 960 of them.

Whether an entry's config is kept depends only on names known before any entry
resolves, so resolve_vendor_preset makes the copy alongside the rest of its work
and commit moves it in.
2026-09-27 08:42:36 -05:00
raistlin7447 18867094fa libslic3r: load the setup wizard's vendors the way startup does
When the wizard's profile-data cache is stale, which any vendor's version change
makes it, the printer and filament selection dialogs rebuild it by loading every
shipped vendor, 66 of them here, one after another. Startup loads its vendors in
parallel once the filament library is in.

That step moves out of load_system_presets_from_json into
PresetBundle::load_vendors, which takes each vendor with the directory it is
installed in, and the wizard calls it too. A vendor that fails to load still
sends the wizard to its hand-written JSON scan, as the exception it used to throw
did, now once the others have loaded and with the error logged. merge_presets
goes private, since the wizard no longer merges bundles itself.

Opening either dialog with a stale cache goes from 3.38-3.55 s to 2.33-2.47 s on
a 16-core desktop, and the vendor rebuild inside it from 2.34-2.48 s to
1.27-1.41 s. The catalog it writes is byte-identical to the one main writes, and
a dump of every preset startup loads is unchanged.
2026-09-27 08:42:36 -05:00
raistlin7447 5ae88ae5c7 libslic3r: install parsed and cached presets through one routine
A cache load installed the entries others inherit from or include first and
resolved the rest together, while the JSON parse installed each entry straight
after parsing it. The two orders only agree while every preset is listed after
what it depends on. An include listed after the preset naming it was missing for
one and found by the other, and every new kind of reference between presets
would have needed the same care in two places.

Both loads now go through install_vendor, which installs processes, filaments and
printers in listing order. Entries resolve together in runs and commit one at a
time, and a run ends before an entry that inherits or includes one already in
it. No entry resolves against a registration from its own run, so the result is
what installing one at a time gives, in any listing order.

The JSON parse reads every sub-file first, under one numeric locale setter, and
stops at the first that fails. The ones before it are installed before the
failure is raised, as they were one at a time, and the filament library's maps
are published only from a complete load. Each entry's parse errors are logged and
counted when it installs, so the log and a load that fails partway come out as
one-at-a-time loading leaves them.

Every preset already in the collection counts as taken for a new one's name.

resolve_vendor_preset and commit_vendor_preset take a VendorInstall with the
collection's shared state in place of a dozen parameters, const for resolve.

The first launch after an update, which parses every profile, goes from
1510-1533 ms to 806-849 ms on a 16-core desktop, and a cached launch from
342-357 ms to 328-334 ms.
2026-09-27 08:42:35 -05:00
raistlin7447 ea3a40d073 libslic3r: move the filament library's config map instead of copying it
Every preset of the filament library keeps its config for other vendors to
inherit from, and publishing that map copied all 1281 of them. The map it is
built in is cleared by the next install before anything reads it again, so the
copy was of no use to anyone.

Loading the library goes from 145 to 86 ms, and the whole preset load from 360
to 300 ms.
2026-09-27 08:42:35 -05:00
raistlin7447 806568287c libslic3r: append a vendor's presets and sort once
Installing a preset placed it at its position in the sorted collection, which
moves every preset after it, so a vendor with 2528 filaments paid an O(n)
insertion 2528 times. That is serial work at the end of a load whose per-preset
part now runs across threads, so it is what the load waits on.

The entries are appended and the collection sorted once they are all in, the way
user presets are already loaded. The sort is on a scope guard, so it also runs
for an entry that cannot be installed and throws past the end of the loop. A
repeated name was caught by find_preset against the half-filled collection,
which an unsorted one cannot answer, so the names are kept in a set, seeded with
the default presets that are in the collection before the first entry arrives.
2026-09-27 08:42:34 -05:00
raistlin7447 0342ddbe26 libslic3r: check only the keys an entry added for validity
A preset's config is a copy of the one it inherits with the entry's diff applied
over it, so the only keys it can hold that the parent does not define are the
ones in that diff. Scanning all of them instead walked 141 keys per filament
preset where 23 would do, and built a vector of every key's name to do it.

The keys the caller knows were added are checked where it has them, and the scan
of everything stays for the callers that do not. An entry that includes a
template is checked the same way, since an include diff holds only keys of the
collection default. The check also moves above Preset::normalize, which derives
keys from the ones it finds. A key derived from one the profile should not have
carried is in neither the diff nor the parent, so after normalize nothing would
name it.
2026-09-27 08:42:34 -05:00
raistlin7447 33af0651b7 libslic3r: resolve user presets together
A user preset is read from its own file and flattened against the system preset
it inherits. It cannot inherit another file of the same pass, because the presets
being read only join the collection once they have all been read, so the files
are independent of each other.

load_presets splits the way the vendor cache load already does:
resolve_user_preset reads and flattens one file and touches nothing shared, and
commit_user_preset installs it, counts its errors, and does the file work a load
can trigger (removing an unreadable preset, writing back a filament preset that
named no compatible printer). Both callers now share resolve_then_commit, which
holds the two-phase shape, works through the items in batches so what is held at
once does not grow with how many there are, and gives each piece of a batch one
CNumericLocalesSetter rather than one per file. It throws rather than commit a
batch that a canceled task group of the caller left unresolved.

A setter nested in another on the same thread does nothing while the locale is
still "C", so one setter per piece of a batch covers every file in it, and each
batch runs isolated, so a thread waiting on it picks up none of the caller's
other work. A filament preset whose derived compatible printer cannot be written
back still loads, with the failure counted.

Loading 328 user presets goes from 105 to 34 ms on a 16-core desktop.
2026-09-27 08:42:33 -05:00
raistlin7447 781a5d8516 libslic3r: compare preset json keys without the locale
Reading one preset file tries a dozen key names against every key in it with
boost::iequals, which compares through std::locale(). Constructing that takes a
lock the whole process shares in the MSVC runtime, so it is both the bulk of the
work in a preset file's parse and the reason two threads cannot do it at once.

Comparing the bytes instead takes loading 328 user presets from 129 to 105 ms
one at a time, and the same files spread across threads from 132 to 27 ms. The
first launch after an update, when the system profiles are parsed from JSON
rather than served from their cache, goes through the same function.
2026-09-27 08:42:32 -05:00
raistlin7447 38446ab77a libslic3r: resolve a vendor's cached presets together
Installing one entry is a config copy, the entry's diff applied over it,
normalization and an invalid-key scan. For the largest vendor that is around
2000 filaments at about 115 us each, and all of it reads state that does not
change while the collection loads.

load_vendor_preset splits into resolve_vendor_preset, which flattens an entry
and touches nothing shared, and commit_vendor_preset, which installs it. A
cache load installs the entries others inherit from first, since what those
register is what the rest resolve against, then resolves the rest together and
commits them one at a time. The JSON parse calls the two back to back as before.

On a 16-core desktop with 4263 system presets installed, preset loading goes
from 644-652 ms to 370-376 ms, and a bundle loaded either way comes out
identical, preset for preset and option for option.
2026-09-27 08:42:32 -05:00
raistlin7447 9080269f6f libslic3r: split the system preset load across threads reliably
The parallel_for over the vendors used an auto_partitioner, which splits its
range only while a worker is asking for work. At six vendors that often did not
happen at all. In two runs out of four on this machine every vendor in that step
ran on the calling thread, which the log shows as one thread id for all of them.

A grain of one splits the range up front. The vendors also start slowest first,
so the vendor that decides when the step ends is not the one left until last. A
vendor with no cache is parsed from its JSONs, which costs far more than any
cache load, so those go before cached ones, and within each group the bigger
file goes first, the cache or the profile index that names the vendor's presets.
2026-09-27 08:42:31 -05:00
d976d9eb0c Release the DC after GetDC in get_dpi_for_window and font enumeration (#15919)
get_dpi_for_window's pre-8.1 fallback and get_font_list_by_enumeration
both called GetDC without a matching ReleaseDC, leaking a GDI handle
each call. get_dpi_for_window runs on every mouse-move over the 3D
viewport, so the leak exhausts the per-process GDI handle limit and
hangs the app within minutes on Windows 7/8.

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

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

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

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

# Screenshots/Recordings/Graphs

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

## Tests

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

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

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

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

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

# Screenshots/Recordings/Graphs

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

## Tests

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

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

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-25 17:53:22 +08:00
Ian Chua 521a30a45c feat: add timestamp to ofl update workflow so that concurrent post_merge_profiles are not silently dropped 2026-09-25 16:53:45 +08:00
Ian Chua 35d5ff705b fix: cronjob checks against last ofl-ota-cronjob instead of post_merge_profiles (#15894)
# Description

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

The previous implementation checks against the last successful
`post_merge_profiles` which can trigger when a normal OTA update for
non-OFL profiles are made. This causes the check to fail when OFL
changes are made before other regular profile changes are made in
`resources/profiles/<vendor>`

# Screenshots/Recordings/Graphs

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

## Tests

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

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

[How to Download Pull Requests Artifacts for
Testing](https://www.orcaslicer.com/wiki/how_to_download_pr_artifacts)
2026-09-25 15:09:41 +08:00
Ian Chua 482fc1e719 fix: cronjob checks against last ofl-ota-cronjob instead of post_merge_profiles 2026-09-25 15:04:32 +08:00
Ian Chua d087941289 test: update profiles for ota update (WILL BE REVERTED) (#15891)
Merged by /bot merge on behalf of @peachismomo (id 52488812).
Grants: resources/profiles/OrcaFilamentLibrary/filament/Elegoo, resources/profiles/OrcaFilamentLibrary.json, resources/profiles/Elegoo, resources/profiles/Elegoo.json
Head: b73e9df4f4
2026-09-25 06:38:09 +00:00
80 changed files with 5685 additions and 1468 deletions
+41 -48
View File
@@ -12,9 +12,9 @@ name: Daily OFL OTA Update
# vendor-dispatch path is also what makes post_merge_profiles.yml call the OTA auto-publish API after
# 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
# table (POST /api/v1/ota/ofl/pending/clear) - the daily "published everything, reset" signal.
# That table is populated only by this pipeline's own auto-publish calls.
# At the start of each run, the pending-publish table is cleared up to a captured
# timestamp (POST /api/v1/ota/ofl/pending/clear?timestamp=...). Changes merged after
# that timestamp remain pending for the next run.
on:
schedule:
@@ -34,6 +34,32 @@ jobs:
if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' }}
runs-on: ubuntu-24.04
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
uses: actions/checkout@v7
with:
@@ -46,13 +72,12 @@ jobs:
run: git fetch origin '+refs/heads/*:refs/remotes/origin/*'
- name: Scan branches and publish changed OFL profiles
id: scan
shell: bash
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SCAN_UNTIL: ${{ steps.start.outputs.timestamp }}
run: |
set -euo pipefail
published_any=false
mapfile -t branches < <(
gh api "repos/${{ github.repository }}/branches" --paginate --jq '.[].name' \
@@ -79,68 +104,36 @@ jobs:
--jq '.workflow_runs[0].run_started_at // empty')"
if [ -z "$since" ]; then
echo "No prior successful run for $branch; treating OFL as changed."
changed=true
else
changed_files="$(git log --since="$since" --name-only --pretty=format: "origin/$branch" -- \
echo "No prior successful run for $branch; checking OFL changes up to $SCAN_UNTIL."
changed_files="$(git log --until="$SCAN_UNTIL" --name-only --pretty=format: "origin/$branch" -- \
resources/profiles/OrcaFilamentLibrary resources/profiles/OrcaFilamentLibrary.json \
| 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
echo "OFL changed on $branch since $since:"
echo "OFL changed on $branch from ${since:-the beginning} through $SCAN_UNTIL:"
echo "$changed_files"
changed=true
else
echo "No OFL changes on $branch since $since."
echo "No OFL changes on $branch through $SCAN_UNTIL."
changed=false
fi
fi
if [ "$changed" = true ]; then
# Tolerate a per-branch failure (e.g. a pre-existing release branch
# whose post_merge_profiles.yml predates the vendor/auto_publish
# 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 }}" \
--ref "$branch" \
-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"
fi
fi
echo "::endgroup::"
done
echo "published_any=$published_any" >> "$GITHUB_OUTPUT"
- name: Clear OFL pending queue
# Only when this run actually kicked off at least one publish - the
# daily reset is scoped to today's real activity, not called on a day
# where every branch reported no changes. Note "published_any" reflects
# a successful DISPATCH, not a confirmed live publish: gh workflow run
# is fire-and-forget, so this workflow never learns whether the
# dispatched post_merge_profiles.yml run actually reached its own
# auto-publish call. Acceptable since the table is populated only by
# our own auto-publish calls, not by anything else.
if: steps.scan.outputs.published_any == 'true'
shell: bash
env:
OTA_API_BASE_URL: ${{ vars.OTA_API_BASE_URL }}
OTA_API_KEY: ${{ secrets.OFL_OTA_PUBLISH_KEY }}
run: |
set -euo pipefail
[ -n "$OTA_API_BASE_URL" ] || { echo "::error::vars.OTA_API_BASE_URL is not set"; exit 1; }
[ -n "$OTA_API_KEY" ] || { echo "::error::secrets.OFL_OTA_PUBLISH_KEY is not set"; exit 1; }
resp_file="$RUNNER_TEMP/ota-pending-clear-response.json"
status="$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST \
"${OTA_API_BASE_URL%/}/api/v1/ota/ofl/pending/clear" \
-H "Authorization: Bearer $OTA_API_KEY")"
body="$(cat "$resp_file")"
echo "$body"
if [ "$status" != "200" ]; then
echo "::error::OTA pending-clear call failed with HTTP $status"
exit 1
fi
+7 -2
View File
@@ -56,9 +56,9 @@ You can do this in Environment Variables settings.
endif ()
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)
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 ()
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
endif ()
@@ -279,6 +279,11 @@ if (APPLE)
endif()
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}")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
set(CMAKE_INSTALL_RPATH "$ORIGIN")
+2 -2
View File
@@ -53,7 +53,7 @@ while getopts ":dpa:snt:xbc:i:j:Tuh" opt; do
echo " -s: Build slicer only"
echo " -u: Build universal app only (requires existing arm64 and x86_64 app bundles)"
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 " -b: Build without reconfiguring CMake"
echo " -c: Set CMake build configuration, default is Release"
@@ -95,7 +95,7 @@ if [ -z "$DEPS_CMAKE_GENERATOR" ]; then
fi
if [ -z "$OSX_DEPLOYMENT_TARGET" ]; then
export OSX_DEPLOYMENT_TARGET="11.3"
export OSX_DEPLOYMENT_TARGET="12.0"
fi
if [ -z "$CMAKE_IGNORE_PREFIX_PATH" ]; then
+2 -2
View File
@@ -26,9 +26,9 @@ endif()
cmake_minimum_required(VERSION 3.2)
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)
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 ()
message(STATUS "CMAKE_OSX_DEPLOYMENT_TARGET: ${CMAKE_OSX_DEPLOYMENT_TARGET}")
+102 -185
View File
@@ -2,219 +2,136 @@
## Why it exists
The main window is a notebook of tabs. When the first frame appears, one tab is on
screen and the others are not; some of them are opened later in the session, some
never, and some only exist for certain printers. Building a tab before the first frame
adds its cost to every startup, whether or not the tab is used.
The main window is a notebook of tabs, and only one of them is on screen when the first
frame appears. Others are opened later in the session, some never, and some only exist
for certain printers. Building every tab before the first frame makes each startup pay for
tabs the user may never open.
This subsystem builds a tab's panel the first time the tab is shown. It also builds the
remaining tabs while the user is idle after startup, in units of tens of milliseconds,
so a click that lands in the middle of one waits for that unit and no longer. Startup
pays only for what the first frame shows, and the other tabs are usually built before
anyone opens them.
This subsystem builds a tab the first time it is shown, and builds the rest in small units
while the user is idle after startup. Startup pays only for what the first frame shows,
the other tabs are usually ready before anyone opens them, and a click that lands in the
middle of the idle build waits for one unit at most. Work that is not a tab, such as a
dialog or the 3D view's GL resources, uses the same machinery.
## The parts
`src/slic3r/GUI/Lazy.hpp`, `LazyPage.hpp`, `StagedBuild.hpp` and `IdleScheduler.hpp/.cpp`
(with its wx-free `PrebuildQueue.hpp`) are independent. A holder can hold anything a
factory makes, a page is a holder with a placeholder widget, a staged object can live
outside a holder, and the scheduler knows none of them; it runs tasks, which `MainFrame`
makes from the holders.
The parts are independent. A holder can hold anything a factory makes, a placeholder page
is a holder with a widget, a staged object can live outside a holder, and the scheduler
knows none of them; it runs tasks, which the main window 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
in the idle queue, both given by the owner. The header has no wx dependency; the busy
cursor for an on-demand build lives in `Lazy.cpp` and is skipped when there is no app,
so the holder is unit-tested.
A holder keeps one object and the factory that makes it. The rest of the app reads the
object if it exists, makes sure it exists now when about to show or navigate to it, or runs
something once it exists. A type with one instance in the app gets these as statics
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.
- `ensure()` builds whatever is left now, under a busy cursor, and returns the object, or
null in the two cases below. A click on an unbuilt tab or a first open of a dialog goes
through this, and it logs the units and time it took.
- `build_step()` runs one unit of construction and returns true while more remain. The
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 guarantees that callers see the object only once it is completely built. A
build cannot re-enter itself, so a nested request finds nothing yet, and a factory that
returns null or a unit that throws leaves the holder and the scheduler able to carry on.
The holder does not own the object; its wx parent does, as for any window. The holder has
no wx dependency and is unit-tested.
The holder does not own the object; its wx parent does, as for any window. A type with
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.
### The placeholder page: `LazyPage<Panel>`
`built()` is an atomic flag, since a job worker reads it through the statics
(`MainFrame::get_calibration_curr_tab()`); it is set after the object is complete.
The notebook needs a page object for a tab to exist and for tabs to be inserted and
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()`,
`build_step()`, `prebuild_order()`) and is what the scheduler side sees.
### Staged construction: `StagedBuild`
### 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
factory makes the panel inside it (by default `new Panel(parent)`). The notebook needs a
page object for the tab to exist and for `show_device()` to insert and remove tabs by
pointer, and the placeholder is that object. `MainFrame` creates every page once, named
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
- members created in steps start out null, so a partly built panel can be destroyed;
- timers, event handlers and destructors that touch step content check that the panel is
complete first;
- nothing takes focus while off screen, since a unit may run while the user is typing
elsewhere;
- where a step's widgets must keep their place in a sizer that later steps also fill, the
constructor adds an empty slot sizer in that position and the step fills the slot
(`StatusBasePanel`).
- a widget added by a step keeps its place in the sizer through an empty slot the skeleton
creates.
### IdleScheduler: when to build
### The scheduler: `IdleScheduler` and `PrebuildQueue`
A task is any `LazyBase`: a name for the log, `pending()`, `build_step()` and
`prebuild_order()`. `PrebuildQueue` holds the tasks by order (equal order in the order
added) and runs a slice, the units of the first pending task until it completes, the
budget is spent on the clock it is given, or the interrupt predicate says input arrived.
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.
The queue holds tasks in order, and a slice runs units of the first pending task until
the task finishes, the time budget is spent, or input arrives. It has no wx dependency and
is unit-tested with a fake clock. A task whose work goes away, such as a tab removed from
the book, is skipped, and becomes pending again if the work comes back.
`IdleScheduler` drives the queue with the real clock, the app's input timestamp and the
Windows queue check, and logs each unit and slice. Each slice is a timer message: a
period of 250 ms while waiting for the user to go quiet, and a one-shot of zero after a
slice that left work, so the event loop dispatches whatever it has queued (paint,
timers, input) before the next slice runs. Chaining slices with `CallAfter` would not do
this: wx drains pending events fully before the next native message, on every platform.
On GTK the one-shot is 5 ms, because a due GLib timeout runs ahead of the redraw and
idle sources that paint and deliver posted events.
A slice runs only once the user has been idle for the quiet time, and the timer stops
itself once no task is pending. The tick period, quiet time and slice length are
constants in `IdleScheduler.cpp`. A unit cannot be interrupted once started, so the
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.
A slice runs only once the user has been idle for a short quiet time, and each slice is its
own timer message, so paint, timers and input queued in between are handled before the
next slice. Posting slices as pending events would not do that, because wx drains every
pending event before the next native message. On GTK a timer that is always due starves
the lower-priority sources that repaint and deliver posted events, so slices are a few
milliseconds apart. On Windows a slice also waits while the native queue holds input,
not counting mouse moves, which Windows synthesizes whenever a window appears under the
cursor. A slice never runs inside a `wxYield()`, where it would build pages in the middle of
the code that yielded. A unit cannot be interrupted, so the largest unit bounds how long a
click can wait.
When nothing is pending the timer stops and the subsystem costs nothing.
The tasks are made by their owners. `MainFrame::prebuild_pages_when_idle()` registers
every `LazyPage` the frame created, in or out of the book (`pending()` is false for a
page out of the book), the Compare presets holder, and the Prepare sidebar's settings
page from `ParamsPanel::settings_page_prebuild()`, whose first unit selects the default
tab if none is selected yet and whose later units build one option group each.
`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.
The main window owns the scheduler because it owns what the tasks build, and clearing the
queue with the window keeps a task from outliving its object. Each owner provides its own
tasks, such as a tab, a dialog, the Prepare tab's settings page one option group at a
time, the Prepare page's layout at the size the book gives its pages, or the 3D view's GL
resources.
Idle time comes from `GUI_App::FilterEvent`, which timestamps mouse and keyboard events
(`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.
### The 3D view's GL resources
Once every registered page is built the timer is stopped and the subsystem costs
nothing.
OpenGL is loaded on the Prepare tab's canvas. When the start page is not Prepare, loading
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
**What builds before the first frame.** The Prepare tab's plater, because `post_init()`
needs its GL canvas on screen to initialise OpenGL in every startup state, and the start
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.
**Before the first frame.** Only the start page and the Prepare tab's plater are built
before the first frame. Everything else goes through a holder.
**Reaching a lazy object.** A caller uses the type's own statics. `T::if_built()` may
return null and is for telling the object something it can live without (a rescale, a
colour change, a status update). `T::ensure()` builds the object and is for navigating
to it or for a caller that is about to show it. A caller that tells the object something
it would not fetch for itself on construction uses `T::when_built()`, which keeps the
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()`.
**Reaching a lazy object.** Callers use the type's statics. Reading it if built is for
things the object can live without, such as a rescale, a color change or a status refresh.
Making sure it exists is for navigating to it or showing it. Running something once it is
built is for state it would not fetch for itself on construction. A panel that pulls its
state when constructed only ever needs to be read if built.
**Unit size.** A unit cannot be interrupted, so it should stay within the slice length on
a fast machine. A constructor above that is staged. A single widget above it is the
floor unless the widget itself is split.
**Unit size.** A unit should fit in one slice on a fast machine. A constructor over that is
staged, and a single widget over it is accepted unless the widget itself can be split.
**Order.** Cheapest and most likely to be opened first, given where `MainFrame` creates
each holder. The settings page is 0 (the Prepare tab's own content), Home 10, Device 20
(a Bambu user's usual second stop), Calibration 30, Multi-device 40, the web Device
view 50, Project 60 (a second WebView2 instance) and the Compare presets dialog 100;
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.
**Order.** Tasks run cheapest and most likely to be opened first. Each holder's order is
given where it is created, with gaps so a new task fits between its neighbors. A negative
order is never prebuilt, for something few sessions open that costs more to build unasked
than it saves.
## 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.
2. A `LazyPage<Panel>*` member in `MainFrame`, created once in `init_tabpanel()` with
its `TAB_ID_*` as the name, its place by the order rule (and a factory if
`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, keep the skeleton in the constructor, move the rest into
steps in its original order, and follow the staged-construction constraints. Measure the
units; a step that is still one big widget is split inside the widget or accepted.
To stage a heavy constructor, inherit `StagedBuild`, keep the skeleton in the constructor,
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.
## Verifying
Verification is by log. `MainFrame::prebuild_pages_when_idle` lists the queue it
registered, `IdleScheduler::tick` reports each completed task by name at info level and
each slice and unit at debug level, and `Lazy::ensure` reports an object a user built
on demand with the units and time it took. A run from the configured start
page shows every registered page complete, in order, with no unit longer than intended.
A click on a tab mid-prebuild shows the finished panel with an `ensure` line for what was
left, and the slices resume for the remaining tasks once the user is idle again.
The log lists the queue when it is registered, reports each completed task at info level
and each slice and unit at debug level, and reports every on-demand build with its units
and time. A task's completion line counts only the slice it finished in. A run from the
configured start page should show every registered task complete in order, with no unit
longer than intended. A click on a tab during the idle build should show an on-demand
build for what was left, with the slices resuming once the user is idle again.
+123
View File
@@ -0,0 +1,123 @@
# Multiline infill — High Level Design
## Purpose and scope
`fill_multiline` prints every sparse infill wall as N adjacent lines instead of
one, so a wall is `d1 = N * spacing` thick. Only internal sparse infill uses it.
Each pattern first builds its single-line centerlines at N times the usual line
spacing (so the density holds), and `multiline_fill()` then replaces each
centerline by the lines of that wall: the centerline itself when N is odd, and
closed outlines around it at every `spacing` out to `d1 / 2`. The outlines are
clipped to the fill region contracted by half a line width, then connected like
any other infill.
Outlines of centerlines that cross each other overlap at every crossing, which
over-extrudes the wall intersections. The line-crossing patterns Grid,
Triangles, Tri-hexagon and Cubic therefore build centerlines that never cross
(`FillRectilinear::fill_surface_trapezoidal()`), and so do Adaptive Cubic and
Support Cubic (`FillAdaptive`); the other patterns outline their usual
centerlines.
## Non-crossing centerlines
The crossing lines are resolved into x-monotone paths, the levels of the line
arrangement: walking along x, the k-th path is always the k-th line from the
bottom. At every crossing, the two paths bounce off each other instead of
passing through. Adjacent paths meet only at crossings, so their outlines touch
there and nowhere overlap.
Where two paths meet, each is cut short by a line perpendicular to the bisector
of its bend, `d1 / 2` from the crossing. The two cut segments are parallel and
`d1` apart, so the outermost lines of the two walls sit exactly `spacing` apart,
like the lines inside a wall. Where three lines meet at one point, the middle
path runs straight through and the outer two are cut `d1` from it.
Each pattern builds its rows along x in a rotated frame. Grid lines run at ±45°
there, and its rows are trapezoid waves that transpose on alternate layers. The three families of Triangles, Tri-hexagon and
Cubic run at 0°, 60° and 120°. Those rows rotate by 120° every layer about a
3-fold center of the arrangement, so each family takes every role in turn.
The pattern is phased on fixed positions, so it lines up across layers and
across the regions of one layer. Rounding the corners with
`sparse_infill_smooth_factor` happens before `multiline_fill()`.
## Cubic
Single-line Cubic draws the three families at the same spacing `h` and shifts
them with z: by `+dx`, `-dx` and `+dx`, `dx = z / sqrt(2)`. The multiline paths
follow the same lines. In the frame where one family is horizontal, the other two
cross in rows `h` apart, alternating by half a period, at height
`tau = -3 * dx (mod h)` above the horizontal line below them. The crossings split
every band between horizontal lines into up-pointing triangles of height `tau`,
down-pointing triangles of height `h - tau`, and hexagons. At `tau = 0` (and `h`)
all three families meet at common points, as in Triangles. At `tau = h / 2` the
triangles are equal, as in Tri-hexagon. The origin of that frame is always a
3-fold center, whatever z is, so the per-layer rotation keeps the lines in place.
Each band holds two paths that touch at its crossings: the upper one takes the
V below the crossing and runs along the top horizontal line, and the lower one
takes the inverted V above it and runs along the bottom line. Both are the same function
of `tau`, the lower one mirrored with `h - tau`. `cubic_upper_level()` builds one
period of the upper path as the lower envelope of five lines, clipped from below:
- the two slanted lines through the crossings,
- the horizontal line, lowered when the triangle above it is less than `1.5 * d1` high,
- the two chamfers where the path turns onto and off the horizontal line, `d1 / 2`
from those crossings,
- the flat cut into the V at the crossing.
The cut height `clamp(tau - d1 / 2, 0, h - d1) + d1` is what makes the pattern
continuous in z. While both triangles are at least `1.5 * d1` high, every
crossing is a pair of bends `d1 / 2` from it, as in Tri-hexagon. When a triangle
is thinner, its three paths stack like a triple crossing. The path through it
flattens toward its base line and lies on it once the triangle is under `d1 / 2`
high, and the paths beside it are pushed `d1` away. The layout thus reaches the
Triangles one where the families meet. Adjacent paths stay at least `d1` apart
at every `tau` and at every density up to 100%.
## Adaptive Cubic
Adaptive Cubic and Support Cubic take their lines from an octree of cubes
standing on a corner. On each layer every cube cuts its three mid-planes into
segments of the same three 60° families as Cubic, but the pattern is not
periodic. Smaller cubes near the surface add finer lines, and a finer line ends
where it meets the wall of its coarser cube, so the lines form crossings and
T-junctions. `FillAdaptive::multiline_paths()` builds the paths from these
segments directly, for each fill region and within `4 * d1` of it.
At a crossing the two paths bounce as in Cubic. At a T-junction the through line
runs straight on and the path of the ending line stops there. Every path still
runs left to right in the frame where one family is horizontal, and that family
rotates with the layer.
Every line of every cube size lies on one fine lattice, so crossings closer than
a few `d1` are the corners of one small triangle of that lattice, as in Cubic.
The cuts follow the Cubic rules without a closed formula:
- The two bends of a crossing are cut `d1` apart, `d1 / 2` each, perpendicular
to their bisector, so their walls touch. A cut goes no further than the path
end, and the other bend takes the rest of `d1`.
- At the tip of a small triangle, between the two slanted families, a cut also
goes no further than the neighbouring bend turning the other way, and the
path beyond that bend is kept a wall away from it. The bends onto the
horizontal family are not limited this way: pushing their paths apart would
open gaps between walls that should touch.
- A cut moves the path only where the cut line lies beyond it, near its bend.
The sharp bends between the two slanted families are cut after the bends onto
the horizontal family, so the tip of a small triangle wins, as in Cubic.
- A path stopping at a T-junction is trimmed until it is `d1` less half a line
spacing from every other path, so that its end overlaps the wall it stops on
by half a line and bonds to it. The paths are trimmed one at a time against
the others as already trimmed, so two ends facing each other meet instead of
both backing off. A path stopping on the line of another is trimmed before
that one, so it gives way and the other still reaches the line it stops on. A
second round trims every path again from its full length, so an end grows
back where the ends it gave way to were trimmed later, and a last round only
shortens them, keeping them that far apart. Paths shorter than `d1` are left
out.
- A line that ends on another less than `2 * d1` past a crossing stops at that
crossing instead, the shorter one where both do. The path along such a stub
would be trimmed away, leaving a hole between the walls that were cut to
touch it.
Short paths enclosed by coarser lines still print as closed outlines, but most
paths run on across several cells.
+45 -19
View File
@@ -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
vendors keep theirs — even when the bumped vendor is the shared Orca filament
library everyone else inherits from.
- The setup wizard, which loads vendors one at a time, gets the same speedup as
startup without a second code path.
- The setup wizard loads its vendors through the same routine as startup, so it
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 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
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
library therefore goes first, alone; every other vendor follows in parallel, resolving
against it; and the results are merged in a stable order:
and nothing else reaches across vendors — an `include` is always vendor-local. Only
installing a vendor's presets crosses it; reading the vendor, from its cache or its
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
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
order — both produce the same bundle, so cached and parsed vendors mix freely in
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.
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
them and the currently loaded filament library, includes layered in, configs flattened
onto the collection defaults, validated and registered — by the same function the JSON
path calls straight after parsing a sub-file. An `include` layers what the included
base states, between the parent and the preset's own keys: the base's diff against the
then installed by `install_vendor`, the routine the JSON path hands the vendor's entries
to once it has parsed the sub-files: inheritance resolved against the presets installed
before them and the currently loaded filament library, includes layered in, configs
flattened onto the collection defaults, validated and registered. An `include` layers
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
`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
@@ -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
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
`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
@@ -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
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
per-vendor caches costs ~2 s of preset installation whatever feeds it, so only
skipping the rebuild entirely wins.
per-vendor caches costs over a second of preset installation whatever feeds it, so
only skipping the rebuild entirely wins.
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;
the wizard then rebuilds the bundle vendor by vendor (per-vendor caches serving where
they cover) and writes the catalog back. Selections, region and per-open decorations
are applied 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 wizard then rebuilds the bundle with `PresetBundle::load_vendors`, the load
startup uses (per-vendor caches serving where they cover), and writes the catalog
back. When a vendor fails to load, the filament library included, that open falls
back to the wizard's own scan of the vendor JSONs, as when no bundle can be built,
and writes nothing. Selections, region and per-open decorations are applied
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
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
`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.
- **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
65535 options and one cache at most 65535 distinct enum value names.
`CacheDictionary::save` throws past that, which surfaces when CI generates the
+197
View File
@@ -0,0 +1,197 @@
# Prime tower sparse layers — High Level Design
## Purpose and scope
A prime tower exists to absorb filament changes, but it is planned on every
object layer below the topmost change, not only on the layers that purge. The
layers in between carry no filament change and print nothing but a block of the
tower's own footprint to keep its top level. They are called sparse layers, and
on a print with few changes they are most of the tower: they cost time, filament
and a travel to the tower on every layer.
Two settings trade that cost against something else. `wipe_tower_no_sparse_layers`
drops them, which sinks the tower below the model. `wipe_tower_sparse_layers_combination`
merges runs of them into fewer, thicker layers, which keeps the tower level with
the model. Both are off by default, and with both off the tower prints one layer
per object layer as it always has.
The decisions belong to tower planning and G-code emission. They do not change
sliced object geometry, but they do change the emitted G-code, the filament and
time estimates, and — for the compacted case — whether a plate is printable at
all. Changing either setting invalidates the tower step.
## What a sparse layer is
`ToolOrdering::fill_wipe_tower_partitions` counts the filament changes per layer
and propagates that count downwards, so every layer below the topmost change is
marked as carrying a tower. It then fills any gap between two tower layers, so
the tower is continuous from the bed to its last purge. `wipe_tower_layer_height`
is the distance from the previous tower layer, which is the object's layer height
whenever the tower prints on every layer.
`Print::_make_wipe_tower` plans one tower layer per such object layer. A layer
whose only call keeps the current filament leaves no toolchange in the plan, and
the layer it generates is a single result whose initial and new tool are equal.
That is what `wipe_tower_layer_is_sparse` recognises, and it is the unit both
settings work on.
The plan stays one entry per tower layer in every case. The G-code emitter walks
`WipeTowerData::tool_changes` by layer index, advancing once per object layer
that carries a tower, so a planner that removed entries would silently shift
every later layer onto the wrong tower geometry. Layers that print nothing are
therefore still planned and still generated; they are marked, and the emitter
drops them.
## Shared rules
Tower planning, G-code emission and the plate validation all have to agree about
which layers print and where. They ask one set of free functions, declared beside
the tower classes, rather than each re-deriving the answer from the raw options:
- `wipe_tower_sparse_layers_skipped` — whether sparse layers are really dropped.
Smooth timelapse and clumping detection park the nozzle on the tower every
layer, so with either of them on no layer is ever dropped and the option reads
as off everywhere.
- `wipe_tower_sparse_layers_combined` — whether runs are really merged. The same
two rule it out, and so does `wipe_tower_no_sparse_layers`: dropping the layers
outright is the stronger answer to the same problem, so the two settings are
exclusive and the GUI greys out the second while the first is on.
- `wipe_tower_layer_is_sparse`, `wipe_tower_layer_is_combined_away` — per-layer
questions the emitter asks about generated results.
- `compute_compacted_wipe_tower_z` — the tower's print z per planned layer when
it is compacted.
- `combine_sparse_wipe_tower_layers` and its `combine_sparse_wipe_tower_plan`
wrapper — the merge rule, applied to either generator's plan.
Both tower generators are driven through these. `WipeTower` (Type 1, the block
tower) and `WipeTower2` (Type 2, the default) keep separate plans with the same
per-layer shape — print z, layer height, toolchanges, and a `combined_away` flag
— so one template covers both.
## Dropping sparse layers
With `wipe_tower_no_sparse_layers`, the tower only grows on layers that carry a
real change. It therefore falls one layer height behind the object for every
sparse layer, and by the top of a tall print it can sit far below the model. The
nozzle has to reach down to it at each purge.
`compute_compacted_wipe_tower_z` derives that z once, from the generated results,
so the emitter and the validator cannot disagree. Emission descends to it, but
only once the nozzle is parked over the tower: descending while still over the
model would drive the nozzle into the print, so a descent that would do that is
deferred until after the travel to the tower. Extrusions emitted without an
explicit z — the nozzle-change wipe in particular — are pulled down to the
compacted z for the same reason.
Reaching down is only safe if nothing tall stands near the tower. `Print.hpp`
carries the clearance rule: a keep-out zone grown from the tower's footprint by
the spiral z-hop envelope, and a per-object limit on how high an object may rise
near it, tiered by the nozzle cone, the head body, the rod and the lid. The same
rule serves the precise check on real extrusions, the pre-slice estimate that
feeds the plater, and the outlines the plater draws while an object is dragged,
so that the ring the user sees touches the object's outline exactly when the
check trips.
## Merging sparse layers
With `wipe_tower_sparse_layers_combination`, no layer is dropped and nothing is
compacted: the tower keeps following the object, and the nozzle never descends.
Instead a run of consecutive sparse layers prints once, on the run's last layer,
at the accumulated height of everything it covers — the same way infill
combination merges sparse infill. The layers below it in the run print nothing.
`combine_sparse_wipe_tower_plan` runs before the tower's depths are planned,
because the heights it rewrites feed the extrusion flow of every later pass. It
raises `height` in place on the layer that prints a run and sets `combined_away`
on the rest; generation then proceeds unchanged, and the flag is copied onto the
results so the emitter can drop them.
Four constraints shape the rule:
- **Whole layers only.** A tower layer is entered at the object's z, so a merged
layer has to end on an object layer boundary. The merged height is therefore a
sum of whole layer heights, never a clamped value.
- **The nozzle's maximum layer height.** A run stops growing as soon as one more
layer would pass `max_layer_height` for the nozzle printing it — three quarters
of the nozzle diameter when that is left at 0, as elsewhere in slicing. The cap
is read through the filament-to-nozzle map, since `max_layer_height` is per
nozzle while the tower indexes filaments. This is what makes the setting inert
at common layer heights: two 0.2 mm layers are 0.4 mm and do not fit under a
0.3 mm maximum, so nothing merges until the layer height is 0.15 mm or below,
or the maximum is raised.
- **A filament change purges at its own z.** A layer with a real change can
neither be merged away nor absorb the run below it, so a run always ends on its
own last sparse layer and the change above it is untouched.
- **The first layer stays on the bed.** It carries the brim and is never merged.
A run holds one filament throughout — that is what makes it sparse — so the cap
is uniform across it, and the tower reserves depth only for the purges above a
layer, so a run has one footprint and the merged layer covers exactly the area
the layers it replaces would have.
## Emission and accounting
`WipeTowerIntegration` drops a layer whose results are marked, for both settings,
through the same `ignore_sparse` path in `tool_change` and
`is_empty_wipe_tower_gcode`. A dropped layer emits no travel to the tower and no
extrusion.
Filament used is accumulated by the generators while they write, so a layer that
will be dropped must not be charged. Type 1 asks `layer_is_printed` at each of
its accumulation points; Type 2 guards the equivalent block in `finish_layer`,
which also stops a merged-away layer from adding height of its own — the layer
that prints the run carries all of it.
A merged layer is the only case where the tower's layer height differs from the
object layer it sits on, and therefore the only case where the height the
exporter already emitted for that layer is wrong for the tower. Both generators
do declare a height, but each hardcodes a tag dialect — the block tower forces
the BBL tag, the other writes the compatible one — while the G-code processor
reads only the tag its printer uses. On a non-BBL printer with a Type 1 tower the
declaration is dropped, and the merged layer is drawn and costed as a thin one.
`WipeTowerIntegration::tower_height_tag` therefore declares it at export time,
where the printer is known, and only when the tower's own G-code does not already
carry the tag that will be read. The object's height returns on the next object
path, because emission forces the processor role to the tower on any layer that
carries one.
## Constraints
A layer that prints nothing prints nothing at all, including any interface work
the tower planner scheduled there. The Type 1 block planner marks a layer as a
contact layer when a filament category stops or starts being used relative to the
layer below, and a sparse layer immediately above a change qualifies. Merging a
run, like dropping its layers, replaces that interface with the run's single
layer. Both settings are off by default for this among other reasons.
Neither setting changes what the tower is for. A plate that needs a tower on
every layer — smooth timelapse, clumping detection — gets one, and the settings
read as off rather than compacting or merging in one place and not another.
## Implementation and verification
- [WipeTower.hpp](../../src/libslic3r/GCode/WipeTower.hpp) declares the shared
rules and the plan-merging template;
[WipeTower.cpp](../../src/libslic3r/GCode/WipeTower.cpp) implements them and
the Type 1 tower, [WipeTower2.cpp](../../src/libslic3r/GCode/WipeTower2.cpp)
the Type 2 tower.
- [ToolOrdering.cpp](../../src/libslic3r/GCode/ToolOrdering.cpp) decides which
layers carry a tower at all, and
[Print.cpp](../../src/libslic3r/Print.cpp) plans it and runs the clearance
check whose rule lives in [Print.hpp](../../src/libslic3r/Print.hpp).
- [GCode.cpp](../../src/libslic3r/GCode.cpp) emits the tower, drops the layers
that print nothing, and declares a merged layer's height;
[PrintConfig.cpp](../../src/libslic3r/PrintConfig.cpp) defines the settings and
[ConfigManipulation.cpp](../../src/slic3r/GUI/ConfigManipulation.cpp) their
mutual exclusion.
- [GLCanvas3D.cpp](../../src/slic3r/GUI/GLCanvas3D.cpp) and
[PartPlate.cpp](../../src/slic3r/GUI/PartPlate.cpp) draw the compacted tower's
keep-out outlines live while the user drags.
- [Rule tests](../../tests/libslic3r/test_wipe_tower.cpp) cover the gating of
both settings, the per-layer predicates, the compacted z, the merge rule's run
flushing, height conservation, the nozzle cap and the first-layer exemption,
and the clearance geometry the plater draws.
- [Slicing tests](../../tests/fff_print/test_wipe_tower.cpp) slice a real print
and check that a run folds, that the tower still covers the object exactly
once, that a run too thin for the cap is left alone, and that a merged layer
declares its height in the tag the printer's processor reads.
+1 -1
View File
@@ -7,7 +7,6 @@
<script type="text/javascript" src="../include/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="../include/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" />
<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="./css/dark.css" />
<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="model.js"></script>
+46 -64
View File
@@ -5,6 +5,7 @@
//BBS
#include "Preset.hpp"
#include "Exception.hpp"
#include "InstanceLock.hpp"
#include "LocalesUtils.hpp"
#include "Thread.hpp"
#include "format.hpp"
@@ -739,10 +740,13 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs)
#ifdef USE_JSON_CONFIG
std::string AppConfig::load()
std::string AppConfig::load(bool read_only)
{
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.
namespace pt = boost::property_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
// to avoid race conditions with multiple instances of Slic3r
const auto path = config_path();
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
json j;
@@ -1122,43 +1125,18 @@ void AppConfig::save()
j["local_machines"][local_machine.first] = m_json;
}
boost::nowide::ofstream c;
c.open(path_pid, std::ios::out | std::ios::trunc);
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);
const std::string config_str = j.dump(1, '\t');
if (write_config_file(path, config_str + "\n", config_str))
m_dirty = false;
}
#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.
namespace pt = boost::property_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
// to avoid race conditions with multiple instances of Slic3r
const auto path = config_path();
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
std::stringstream config_ss;
if (m_mode == EAppMode::Editor)
@@ -1332,39 +1309,39 @@ void AppConfig::save()
// One empty line before the MD5 sum.
config_ss << std::endl;
std::string config_str = config_ss.str();
boost::nowide::ofstream c;
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);
const std::string config_str = config_ss.str();
if (write_config_file(path, config_str, config_str))
m_dirty = false;
}
#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
{
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()
{
#ifdef USE_JSON_CONFIG
@@ -1889,7 +1871,7 @@ bool AppConfig::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
+16 -1
View File
@@ -2,6 +2,7 @@
#define slic3r_AppConfig_hpp_
#include <set>
#include <chrono>
#include <map>
#include <string>
#include "nlohmann/json.hpp"
@@ -122,14 +123,18 @@ public:
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
// 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.
// The CLI's load: it never saves, so it takes no lock and creates no lock file.
std::string load_if_exists();
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
void save();
// Does this config need to be saved?
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; }
@@ -339,6 +344,8 @@ public:
// Get the default config path from Slic3r::data_dir().
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)
bool legacy_datadir() const { return m_legacy_datadir; }
@@ -449,8 +456,16 @@ private:
// Preset for each machine
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?
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
Semver m_orig_version;
// Whether the existing version is before system profiles & configuration updating
+3
View File
@@ -304,6 +304,8 @@ set(lisbslic3r_sources
Geometry/VoronoiUtils.cpp
Geometry/VoronoiUtils.hpp
Geometry/VoronoiVisualUtils.hpp
InstanceLock.cpp
InstanceLock.hpp
Int128.hpp
KDTreeIndirect.hpp
Layer.cpp
@@ -353,6 +355,7 @@ set(lisbslic3r_sources
Optimize/Optimizer.hpp
Orient.cpp
Orient.hpp
ParallelResolve.hpp
ParameterUtils.cpp
ParameterUtils.hpp
pchheader.cpp
+29 -18
View File
@@ -849,6 +849,19 @@ ConfigSubstitutions ConfigBase::load_from_json(const std::string &file, ForwardC
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)
{
json j;
@@ -916,44 +929,44 @@ int ConfigBase::load_from_json(const std::string &file, ConfigSubstitutionContex
}
//parse the json elements
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());
}
else if (boost::iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
else if (ascii_iequals(it.key(), BBL_JSON_KEY_IS_CUSTOM)) {
//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());
if (it.value() == "project_settings")
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());
}
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());
}
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());
}
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());
}
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());
}
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());
}
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());
}
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());
}
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());
} 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());
} else {
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.
std::ostringstream ss;
this->save_to_json(ss, name, from, version);
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << ss.str();
c.close();
if (const std::error_code ec = write_file_atomically(file, ss.str()))
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": failed to save config to %1%: %2%") % file % ec.message();
else
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
}
+6 -3
View File
@@ -697,7 +697,8 @@ public:
}
} else {
// 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())
this->values.resize(rhs_vec->size());
else
this->values.resize(rhs_vec->size(), this->values.front());
else {
T v = this->values.front();
this->values.resize(rhs_vec->size(), v);
}
assert(default_index.size() == rhs_vec->size());
+1
View File
@@ -990,6 +990,7 @@ EmbossStyles Emboss::get_font_list_by_enumeration() {
std::vector<std::wstring> font_names;
EnumFontFamilies(hDC, (LPCTSTR) NULL, EnumFamCallBack,
(LPARAM) &font_names);
ReleaseDC(NULL, hDC);
EmbossStyles font_list;
for (const std::wstring &font_name : font_names) {
+29 -4
View File
@@ -464,6 +464,16 @@ void group_region_by_fuzzify(PerimeterGenerator& g)
}
}
g.fuzzy_supported_area.reset();
if ((g.has_fuzzy_skin || g.has_fuzzy_hole) && g.lower_slices != nullptr) {
coord_t max_thickness = 0;
for (const auto& region : regions)
if (should_fuzzify(region.config, g.layer_id, 0, true) || should_fuzzify(region.config, g.layer_id, 0, false))
max_thickness = std::max(max_thickness, region.config.thickness);
// Walls farther than a line width plus the noise amplitude from the layer below are bridging; keep them smooth.
g.fuzzy_supported_area = offset_ex(*g.lower_slices, float(g.ext_perimeter_flow.scaled_width() + max_thickness));
}
if (regions.size() == 1) { // optimization
g.regions_by_fuzzify.push_back({regions.front().config, {}});
return;
@@ -560,13 +570,23 @@ static std::vector<MergedFuzzyRegion> collect_merged_fuzzy_regions(const std::ve
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 fuzzified;
const auto slice_z = perimeter_generator.slice_z;
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 bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, loop_idx, is_contour);
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
if (merged_regions.size() == 1) {
const auto& mr = merged_regions.front();
if (mr.expolygons.empty()) {
if (mr.expolygons.empty() && !supported) {
fuzzified = polygon;
fuzzy_polyline(fuzzified.points, true, slice_z, *mr.config);
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())
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
fuzzified = polygon;
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 layer_height = perimeter_generator.layer_height;
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 bool fuzzify = should_fuzzify(config, perimeter_generator.layer_id, extrusion->inset_idx, is_contour);
if (fuzzify)
@@ -703,7 +726,7 @@ void apply_fuzzy_skin(Arachne::ExtrusionLine* extrusion, const PerimeterGenerato
if (!merged_regions.empty()) {
// 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);
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())
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
for (const auto& r : merged_regions) {
const auto splitted = Algorithm::split_line(*extrusion, r.expolygons, false);
+572
View File
@@ -1,3 +1,4 @@
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp"
#include "../ExPolygon.hpp"
#include "../Surface.hpp"
@@ -14,7 +15,9 @@
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <functional>
#include <numeric>
#include <tuple>
// Boost pool: Don't use mutexes to synchronize memory allocation.
#define BOOST_POOL_NO_MT
@@ -1318,6 +1321,564 @@ bool has_no_collinear_lines(const Polylines &polylines)
}
#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(
const FillParams &params,
unsigned int thickness_layers,
@@ -1371,6 +1932,17 @@ void Filler::_fill_surface_single(
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 }; });
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
multiline_fill(all_polylines, params, spacing);
+3
View File
@@ -48,6 +48,9 @@ FillAdaptive::OctreePtr build_octree(
// If true, octree is densified below internal overhangs only.
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
// Cura Engine's class SubDivCube
+99 -1
View File
@@ -3047,12 +3047,56 @@ bool FillRectilinear::fill_surface_by_multilines(const Surface *surface, FillPar
return true;
}
// 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(
const Surface* surface,
FillParams params,
const std::initializer_list<SweepParams>& sweep_params,
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);
@@ -3350,6 +3394,55 @@ bool FillRectilinear::fill_surface_trapezoidal(
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:
// Handle unknown pattern type
break;
@@ -3532,6 +3625,11 @@ Polylines FillStars::fill_surface(const Surface *surface, const FillParams &para
Polylines FillCubic::fill_surface(const Surface *surface, const FillParams &params)
{
Polylines 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;
if (! this->fill_surface_by_multilines(
surface, params,
+179 -169
View File
@@ -970,6 +970,27 @@ static std::vector<Vec2d> get_path_of_change_filament(const Print& print)
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
{
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));
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);
gcode += tower_height_tag(gcodegen, tcr, tcr_gcode);
gcode += tcr_gcode;
// Count the toolchange only when the emitted block really changed the tool —
// 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,
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);
gcode += tower_height_tag(gcodegen, tcr, tcr_gcode);
gcode += tcr_gcode;
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,
// resulting in a wipe tower with sparse layers.
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) {
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;
@@ -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,
// resulting in a wipe tower with sparse layers.
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) {
ignore_sparse = wipe_tower_layer_is_sparse(m_tool_changes[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())
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)
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())
m_small_area_infill_flow_compensator = make_unique<SmallAreaInfillFlowCompensator>(print.config());
// Process file_start_gcode - written at the very top of the file, before any header
{
std::string top_gcode_template = print.config().file_start_gcode.value;
if (!top_gcode_template.empty()) {
DynamicConfig top_config;
// file_start_gcode runs before the parser copy that normally restores these, so set them here.
PlaceholderParser::update_timestamp(top_config);
PlaceholderParser::update_user_name(top_config);
top_config.set_key_value("print_time_total_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Total_Sec_Placeholder)));
top_config.set_key_value("print_time_day", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Day_Placeholder)));
top_config.set_key_value("print_time_hour", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Hour_Placeholder)));
top_config.set_key_value("print_time_minute", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Minute_Placeholder)));
top_config.set_key_value("print_time_sec", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Print_Time_Sec_Placeholder)));
top_config.set_key_value("used_filament_length", new ConfigOptionString(GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Used_Filament_Length_Placeholder)));
std::string top_gcode = print.placeholder_parser().process(top_gcode_template, 0, &top_config);
if (!top_gcode.empty())
file.writeln(top_gcode);
}
}
// Orca: Don't output Header block if BTT thumbnail is identified in the list
// Get the thumbnails value as a string
std::string thumbnails_value = print.config().option<ConfigOptionString>("thumbnails")->value;
// search string for the BTT_TFT label
bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos);
if(!has_BTT_thumbnail){
file.write_format("; HEADER_BLOCK_START\n");
// Write information on the generator.
file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str());
if (is_bbl_printers)
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str());
//BBS: total layer number
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str());
//Orca: extra check for bbl printer
if (is_bbl_printers) {
if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode
// list all label_object_id with sorted order here
m_enable_exclude_object = true;
m_label_objects_ids.clear();
m_label_objects_ids.reserve(print.num_object_instances());
for (const PrintObject *print_object : print.objects())
for (const PrintInstance &print_instance : print_object->instances())
m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id());
std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end());
std::string objects_id_list = "; model label id: ";
for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++)
objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n"));
file.writeln(objects_id_list);
} else {
m_enable_exclude_object = false;
m_label_objects_ids.clear();
}
}
{
std::string filament_density_list = "; filament_density: ";
(filament_density_list+=m_config.filament_density.serialize()) +='\n';
file.writeln(filament_density_list);
std::string filament_diameter_list = "; filament_diameter: ";
(filament_diameter_list += m_config.filament_diameter.serialize()) += '\n';
file.writeln(filament_diameter_list);
coordf_t max_height_z = -1;
for (const auto& object : print.objects())
max_height_z = std::max(object->layers().back()->print_z, max_height_z);
std::ostringstream max_height_z_tip;
max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n';
file.writeln(max_height_z_tip.str());
}
{
auto used_filaments = print.get_slice_used_filaments(false);
std::ostringstream out;
out << "; filament: ";
for (size_t idx = 0; idx < used_filaments.size(); ++idx) {
if (idx != 0)
out << ',';
out << used_filaments[idx] + 1;
}
file.writeln(out.str());
}
file.write_format("; HEADER_BLOCK_END\n\n");
}
// BBS: write global config at the beginning of gcode file because printer
// need these config information
// Append full config, delimited by two 'phony' configuration keys
// CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured
// as configuration key / value pairs to be parsable by older versions of
// PrusaSlicer G-code viewer.
{
if (is_bbl_printers && !skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
// SoftFever: write compatiple image
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n",
first_layer_bed_temperature);
file.write_format(
"; first_layer_temperature = %d\n",
print.config().nozzle_temperature_initial_layer.get_at(0));
file.write("; CONFIG_BLOCK_END\n\n");
} else if (thumbnail_cb != nullptr) {
// generate the thumbnails
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = format("Invalid thumbnails value:");
error_str += GCodeThumbnails::get_error_string(errors);
throw Slic3r::ExportError(error_str);
}
if (!thumbnails.empty())
GCodeThumbnails::export_thumbnails_to_file(
thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); });
}
}
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
const double layer_height = first_object->config().layer_height.value;
const double initial_layer_print_height = print.config().initial_layer_print_height.value;
for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) {
const PrintRegion &region = print.get_print_region(region_id);
file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width());
file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width());
file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width());
file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width());
file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width());
if (print.has_support_material())
file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width());
if (print.config().initial_layer_line_width.value > 0)
file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width());
file.write_format("\n");
}
file.write_format("; EXECUTABLE_BLOCK_START\n");
// SoftFever
if( m_enable_exclude_object)
file.write(set_object_info(&print));
// adds tags for time estimators
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str());
// Prepare the helper object for replacing placeholders in custom G-code and output filename.
m_placeholder_parser_integration.parser = print.placeholder_parser();
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
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.
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
update_placeholder_parser_with_variant_params();
// Expand the file header only after the start-up placeholders and writer state are ready.
// Use the regular custom G-code path so invalid placeholders report the template name.
if (!print.config().file_start_gcode.value.empty())
file.writeln(this->placeholder_parser_process("file_start_gcode", print.config().file_start_gcode.value, initial_extruder_id));
// Orca: Don't output Header block if BTT thumbnail is identified in the list
// Get the thumbnails value as a string
std::string thumbnails_value = print.config().option<ConfigOptionString>("thumbnails")->value;
// search string for the BTT_TFT label
bool has_BTT_thumbnail = (thumbnails_value.find("BTT_TFT") != std::string::npos);
if(!has_BTT_thumbnail){
file.write_format("; HEADER_BLOCK_START\n");
// Write information on the generator.
file.write_format("; generated by %s on %s\n", Slic3r::header_slic3r_generated().c_str(), Slic3r::Utils::local_timestamp().c_str());
if (is_bbl_printers)
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Estimated_Printing_Time_Placeholder).c_str());
//BBS: total layer number
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Total_Layer_Number_Placeholder).c_str());
//Orca: extra check for bbl printer
if (is_bbl_printers) {
if (print.calib_params().mode == CalibMode::Calib_None) { // Don't support skipping in cali mode
// list all label_object_id with sorted order here
m_enable_exclude_object = true;
m_label_objects_ids.clear();
m_label_objects_ids.reserve(print.num_object_instances());
for (const PrintObject *print_object : print.objects())
for (const PrintInstance &print_instance : print_object->instances())
m_label_objects_ids.push_back(print_instance.model_instance->get_labeled_id());
std::sort(m_label_objects_ids.begin(), m_label_objects_ids.end());
std::string objects_id_list = "; model label id: ";
for (auto it = m_label_objects_ids.begin(); it != m_label_objects_ids.end(); it++)
objects_id_list += (std::to_string(*it) + (it != m_label_objects_ids.end() - 1 ? "," : "\n"));
file.writeln(objects_id_list);
} else {
m_enable_exclude_object = false;
m_label_objects_ids.clear();
}
}
{
std::string filament_density_list = "; filament_density: ";
(filament_density_list+=m_config.filament_density.serialize()) +='\n';
file.writeln(filament_density_list);
std::string filament_diameter_list = "; filament_diameter: ";
(filament_diameter_list += m_config.filament_diameter.serialize()) += '\n';
file.writeln(filament_diameter_list);
coordf_t max_height_z = -1;
for (const auto& object : print.objects())
max_height_z = std::max(object->layers().back()->print_z, max_height_z);
std::ostringstream max_height_z_tip;
max_height_z_tip<<"; max_z_height: " << std::fixed << std::setprecision(2) << max_height_z << '\n';
file.writeln(max_height_z_tip.str());
}
{
auto used_filaments = print.get_slice_used_filaments(false);
std::ostringstream out;
out << "; filament: ";
for (size_t idx = 0; idx < used_filaments.size(); ++idx) {
if (idx != 0)
out << ',';
out << used_filaments[idx] + 1;
}
file.writeln(out.str());
}
file.write_format("; HEADER_BLOCK_END\n\n");
}
// BBS: write global config at the beginning of gcode file because printer
// need these config information
// Append full config, delimited by two 'phony' configuration keys
// CONFIG_BLOCK_START and CONFIG_BLOCK_END. The delimiters are structured
// as configuration key / value pairs to be parsable by older versions of
// PrusaSlicer G-code viewer.
{
if (is_bbl_printers && !skip_config_block) {
file.write("; CONFIG_BLOCK_START\n");
std::string full_config;
append_full_config(print, full_config);
if (!full_config.empty())
file.write(full_config);
// SoftFever: write compatiple image
int first_layer_bed_temperature = get_bed_temperature(0, true, print.config().curr_bed_type);
file.write_format("; first_layer_bed_temperature = %d\n",
first_layer_bed_temperature);
file.write_format(
"; first_layer_temperature = %d\n",
print.config().nozzle_temperature_initial_layer.get_at(0));
file.write("; CONFIG_BLOCK_END\n\n");
} else if (thumbnail_cb != nullptr) {
// generate the thumbnails
auto [thumbnails, errors] = GCodeThumbnails::make_and_check_thumbnail_list(print.full_print_config());
if (errors != enum_bitmask<ThumbnailError>()) {
std::string error_str = format("Invalid thumbnails value:");
error_str += GCodeThumbnails::get_error_string(errors);
throw Slic3r::ExportError(error_str);
}
if (!thumbnails.empty())
GCodeThumbnails::export_thumbnails_to_file(
thumbnail_cb, print.get_plate_index(), thumbnails, [&file](const char* sz) { file.write(sz); }, [&print]() { print.throw_if_canceled(); });
}
}
// Write some terse information on the slicing parameters.
const PrintObject *first_object = print.objects().front();
const double layer_height = first_object->config().layer_height.value;
const double initial_layer_print_height = print.config().initial_layer_print_height.value;
for (size_t region_id = 0; region_id < print.num_print_regions(); ++ region_id) {
const PrintRegion &region = print.get_print_region(region_id);
file.write_format("; external perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frExternalPerimeter, layer_height).width());
file.write_format("; perimeters extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, layer_height).width());
file.write_format("; infill extrusion width = %.2fmm\n", region.flow(*first_object, frInfill, layer_height).width());
file.write_format("; solid infill extrusion width = %.2fmm\n", region.flow(*first_object, frSolidInfill, layer_height).width());
file.write_format("; top infill extrusion width = %.2fmm\n", region.flow(*first_object, frTopSolidInfill, layer_height).width());
if (print.has_support_material())
file.write_format("; support material extrusion width = %.2fmm\n", support_material_flow(first_object).width());
if (print.config().initial_layer_line_width.value > 0)
file.write_format("; first layer extrusion width = %.2fmm\n", region.flow(*first_object, frPerimeter, initial_layer_print_height, true).width());
file.write_format("\n");
}
file.write_format("; EXECUTABLE_BLOCK_START\n");
// SoftFever
if( m_enable_exclude_object)
file.write(set_object_info(&print));
// adds tags for time estimators
file.write_format(";%s\n", GCodeProcessor::reserved_tag(GCodeProcessor::ETags::First_Line_M73_Placeholder).c_str());
// Emit machine envelope limits for the Marlin firmware.
this->print_machine_envelope(file, print);
// Disable fan.
if (m_config.auxiliary_fan.value && print.config().close_fan_the_first_x_layers.get_at(initial_extruder_id)) {
file.write(m_writer.set_fan(0));
//BBS: disable additional fan
file.write(m_writer.set_additional_fan(0));
}
std::string machine_start_gcode = this->placeholder_parser_process("machine_start_gcode", print.config().machine_start_gcode.value, initial_extruder_id);
if (print.config().gcode_flavor != gcfKlipper) {
// Set bed temperature if the start G-code does not contain any bed temp control G-codes.
+6 -1
View File
@@ -107,7 +107,8 @@ public:
m_is_first_print(true),
m_print_config(&print_config),
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
// the emitter cannot disagree about where the compacted tower sits on any given layer.
@@ -138,6 +139,7 @@ public:
private:
WipeTowerIntegration& operator=(const WipeTowerIntegration&);
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;
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;
@@ -175,6 +177,9 @@ private:
// 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.
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.
std::vector<float> m_compacted_tower_z;
};
+73 -5
View File
@@ -49,6 +49,48 @@ std::vector<float> compute_compacted_wipe_tower_z(const std::vector<std::vector<
return tower_z;
}
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)
{
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(10.f),
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_travel_speed(config.travel_speed.get_at(get_extruder_index(config, (unsigned int)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));
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));
if (max_vol_speed!= 0.f)
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.
// 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();
@@ -3898,7 +3951,7 @@ WipeTower::ToolChangeResult WipeTower::finish_layer_new(bool extrude_perimeter,
// 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.
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();
@@ -4008,7 +4061,7 @@ WipeTower::ToolChangeResult WipeTower::finish_block(const WipeTowerBlock &block,
// 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.
if (!m_sparse_layers_skipped || toolchanges_on_layer)
if (layer_is_printed(toolchanges_on_layer))
if (filament_id < m_used_filament_length.size())
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.
// 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())
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;
}
// 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()
{
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())
return;
//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
plan_tower_new();
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()) {
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));
}
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.
// 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();
return construct_tcr(writer, false, old_tool, true, false, 0.f, false);
+67
View File
@@ -152,6 +152,10 @@ public:
bool is_contact = false;
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.
float total_extrusion_length_in_plane() {
float e_length = 0.f;
@@ -391,6 +395,8 @@ public:
float filament_tower_interface_pre_extrusion_dist = 0;
float filament_tower_interface_pre_extrusion_length = 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_bridging = 0.f;
bool m_sparse_layers_skipped = false;
bool m_sparse_layers_combined = false;
// BBS: remove useless config
//bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
@@ -595,6 +602,8 @@ private:
}
// Calculates depth for all layers and propagates them downwards
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
void make_wipe_tower_square();
@@ -634,6 +643,8 @@ private:
float depth; // depth of the layer based on all layers above
float extra_spacing;
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; }
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);
// 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
#endif // WipeTowerPrusaMM_hpp_
+22 -1
View File
@@ -1033,6 +1033,7 @@ WipeTower2::WipeTower2(const PrintConfig& config, const PrintRegionConfig& defau
m_z_pos(0.f),
m_bridging(float(config.wipe_tower_bridging)),
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_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))),
@@ -1150,6 +1151,16 @@ void WipeTower2::set_extruder(size_t idx, const PrintConfig& config)
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
// 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));
if (max_vol_speed!= 0.f)
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.
// 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())
m_used_filament_length[m_current_tool] += writer.get_and_reset_used_filament_length();
m_current_height += m_layer_info->height;
@@ -2435,6 +2448,10 @@ void WipeTower2::generate(std::vector<std::vector<WipeTower::ToolChangeResult>>
if (m_plan.empty())
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();
#if 1
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);
}
if (layer.combined_away)
for (WipeTower::ToolChangeResult &tcr : layer_result)
tcr.combined_away = true;
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)
+5
View File
@@ -200,6 +200,8 @@ public:
float tower_interface_pre_extrusion_length = 0.f;
float tower_ironing_area = 4.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:
@@ -268,6 +270,7 @@ private:
float m_extra_loading_move = 0.f;
float m_bridging = 0.f;
bool m_sparse_layers_skipped = false;
bool m_sparse_layers_combined = false;
bool m_set_extruder_trimpot = false;
bool m_adhesion = true;
GCodeFlavor m_gcode_flavor;
@@ -368,6 +371,8 @@ private:
float z; // z position of the layer
float height; // layer height
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; }
std::vector<ToolChange> tool_changes;
+165
View File
@@ -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
+60
View File
@@ -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
+16
View File
@@ -13,8 +13,19 @@
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()
{
// 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
_configthreadlocale(_ENABLE_PER_THREAD_LOCALE);
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);
uselocale(m_new_locale);
#endif
// Counted last, since the destructor does not run for a constructor that throws.
++ s_numeric_locale_depth;
}
CNumericLocalesSetter::~CNumericLocalesSetter()
{
-- s_numeric_locale_depth;
if (m_nested)
return;
#ifdef _WIN32
std::setlocale(LC_NUMERIC, m_orig_numeric_locale.data());
#else
+5
View File
@@ -19,8 +19,13 @@ class CNumericLocalesSetter {
public:
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:
// Inside another setter on this thread, which does the setting and restoring.
bool m_nested { false };
#ifdef _WIN32
std::string m_orig_numeric_locale;
#else
+65
View File
@@ -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
+37 -12
View File
@@ -2256,13 +2256,45 @@ void PerimeterGenerator::process_no_bridge(Surfaces& all_surfaces, coord_t perim
// ORCA:
// 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.
*
* @param entities The list of PerimeterGeneratorArachneExtrusion entities.
* @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_internal The distance threshold to consider for proximity for a reference perimeter with inset index 1+
* @param threshold_external The minimum touching distance for a reference perimeter with inset index 0
* @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).
* @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) {
const auto& referenceEntity = entities[refIdx];
Points referencePoints = Arachne::to_points(*referenceEntity.extrusion);
for (size_t i = 0; i < entities.size(); ++i) {
// Skip already considered references and the reference entity
if (referenceIndices.count(i) > 0) continue;
@@ -2282,15 +2313,9 @@ std::vector<int> findAllTouchingPerimeters(const std::vector<PerimeterGeneratorA
continue; // skip if they dont match
}
Points points = Arachne::to_points(*entity.extrusion);
double distance = MultiPoint::minimumDistanceBetweenLinesDefinedByPoints(referencePoints, points);
// Add to touchingIndices if within threshold distance
size_t threshold=0;
if(referenceEntity.extrusion->inset_idx == 0)
threshold = threshold_external;
else
threshold = threshold_internal;
if (distance <= threshold) {
// Add to touchingIndices if the lines touch.
const double threshold = double(referenceEntity.extrusion->inset_idx == 0 ? threshold_external : threshold_internal);
if (arachne_lines_touch(*referenceEntity.extrusion, *entity.extrusion, threshold)) {
touchingIndices.insert(i);
}
}
+3
View File
@@ -2,6 +2,7 @@
#define slic3r_PerimeterGenerator_hpp_
#include "libslic3r.h"
#include <optional>
#include <vector>
#include "Layer.hpp"
#include "Flow.hpp"
@@ -105,6 +106,8 @@ public:
bool has_fuzzy_hole = false;
// Preserve construction order so overlap precedence remains deterministic.
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(
// Input:
+352 -168
View File
@@ -5,6 +5,8 @@
#include "Preset.hpp"
#include "PresetBundle.hpp"
#include "AppConfig.hpp"
#include "LocalesUtils.hpp"
#include "ParallelResolve.hpp"
#ifdef _MSC_VER
#define WIN32_LEAN_AND_MEAN
@@ -49,6 +51,9 @@
#include "libslic3r.h"
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "InstanceLock.hpp"
#include <sstream>
#include "Time.hpp"
#include "PlaceholderParser.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)
{
const auto pos = canonical_name.find_last_of('/');
@@ -506,10 +537,11 @@ void Preset::normalize(DynamicPrintConfig &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;
for (const std::string &key : config.keys())
for (const std::string &key : (added != nullptr ? *added : config).keys())
if (! default_config.has(key)) {
if (incorrect_keys.empty())
incorrect_keys = key;
@@ -621,7 +653,6 @@ void Preset::load_info(const std::string& file)
}
else if (v.first.compare("base_id") == 0) {
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)
this->base_id.clear();
}
@@ -647,18 +678,20 @@ void Preset::save_info(std::string file)
file = idx_file.string();
}
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
std::string sync_info_to_save;
//BBS: hold is used for stop requesting to server this time
if (this->sync_info.compare("hold") != 0)
sync_info_to_save = this->sync_info;
std::ostringstream c;
c << "sync_info" << " = " << sync_info_to_save << std::endl;
c << "user_id" << " = " << this->user_id << std::endl;
c << "setting_id" << " = " << this->setting_id << std::endl;
c << "base_id" << " = " << this->base_id << 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)
@@ -667,6 +700,7 @@ void Preset::remove_files(bool cloud_already_deleted)
if (this->is_project_embedded) {
return;
}
InstanceLock instance_lock(user_presets_lock_path());
// Erase the preset file.
boost::nowide::remove(this->file.c_str());
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
void Preset::save(DynamicPrintConfig* parent_config)
bool Preset::save(DynamicPrintConfig* parent_config)
{
//BBS: add project embedded preset logic
if (this->is_project_embedded)
return;
return true;
//BBS: change to json format
//this->config.save(this->file);
std::string from_str;
@@ -703,12 +737,16 @@ void Preset::save(DynamicPrintConfig* parent_config)
else
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);
// 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
if (parent_config) {
DynamicPrintConfig temp_config;
std::vector<std::string> dirty_options = config.diff(*parent_config);
std::string extruder_id_name, extruder_variant_name;
@@ -744,13 +782,22 @@ void Preset::save(DynamicPrintConfig* parent_config)
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()) {
DynamicPrintConfig temp_config = config;
temp_config = config;
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());
} else {
this->config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
to_save = &temp_config;
}
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;
@@ -760,6 +807,7 @@ void Preset::save(DynamicPrintConfig* parent_config)
idx_file.replace_extension(".info");
this->save_info(idx_file.string());
}
return true;
}
void Preset::reload(Preset const &parent)
@@ -771,6 +819,7 @@ void Preset::reload(Preset const &parent)
std::string reason;
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
try {
InstanceLock instance_lock(user_presets_lock_path());
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
this->config = parent.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_cooldown_during_tower",
"wipe_tower_no_sparse_layers",
"wipe_tower_sparse_layers_combination",
"compatible_printers",
"compatible_printers_condition",
"inherits",
@@ -1668,6 +1718,186 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
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.
// Throws an exception on error.
void PresetCollection::load_presets(
@@ -1705,6 +1935,8 @@ void PresetCollection::load_presets(
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);
struct UserPresetFile { fs::path path; std::string canonical_name; };
std::vector<UserPresetFile> files;
//BBS: change to json format
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;
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 {
Preset preset(m_type, canonical_name, false);
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;
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, leave_files);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
}
}
}
});
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()));
sort_presets();
@@ -2166,7 +2296,10 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
preset->setting_id = setting_id;
if (update_time > 0)
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;
}
}
@@ -2826,6 +2959,18 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
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)
{
lock();
@@ -3845,28 +3990,53 @@ bool PresetCollection::select_preset_by_name_strict(const std::string &name)
return false;
}
// Merge one vendor's presets with the other vendor's presets, report duplicates.
std::vector<std::string> PresetCollection::merge_presets(PresetCollection &&other, const VendorMap &new_vendors)
std::vector<std::vector<std::string>> PresetCollection::merge_presets(const std::vector<PresetCollection*> &others, const VendorMap &new_vendors)
{
std::vector<std::string> duplicates;
for (Preset &preset : other.m_presets) {
if (preset.is_default || preset.is_external)
continue;
Preset key(m_type, preset.name);
auto it = (m_type == Preset::TYPE_FILAMENT)
? std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key, filament_preset_less)
: std::lower_bound(m_presets.begin() + m_num_default_presets, m_presets.end(), key);
if (it == m_presets.end() || it->name != preset.name) {
auto less = [this](const Preset &a, const Preset &b) {
return m_type == Preset::TYPE_FILAMENT ? filament_preset_less(a, b) : a < b;
};
struct Incoming { Preset *preset; size_t source; };
auto incoming_less = [&less](const Incoming &a, const Incoming &b) { return less(*a.preset, *b.preset); };
// Each of `others` is sorted, so its presets form one sorted run.
std::vector<Incoming> incoming;
std::vector<size_t> run_ends { 0 };
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) {
// Re-assign a pointer to the vendor structure in the new PresetBundle.
auto it = new_vendors.find(preset.vendor->id);
assert(it != new_vendors.end());
preset.vendor = &it->second;
}
m_presets.emplace(it, std::move(preset));
} else
duplicates.emplace_back(std::move(preset.name));
merged.emplace_back(std::move(preset));
}
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;
}
@@ -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)
{
InstanceLock instance_lock(user_presets_lock_path());
// rename the file
boost::nowide::rename(file_name_from.data(), file_name_to.data());
this->file = file_name_to;
@@ -4344,6 +4521,7 @@ void PhysicalPrinterCollection::load_printers(
continue;
}
try {
InstanceLock instance_lock(user_presets_lock_path());
PhysicalPrinter printer(name, this->default_config());
printer.file = dir_entry.path().string();
// 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;
// Erase the preset file.
{
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(printer.file.c_str());
}
m_printers.erase(it);
return true;
}
@@ -4548,7 +4729,10 @@ bool PhysicalPrinterCollection::delete_selected_printer()
const PhysicalPrinter& printer = this->get_selected_printer();
// Erase the preset file.
{
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(printer.file.c_str());
}
// Remove the preset from the list.
m_printers.erase(m_printers.begin() + m_idx_selected);
// unselect all printers
+74 -5
View File
@@ -8,6 +8,7 @@
#include <unordered_set>
#include <functional>
#include <mutex>
#include <optional>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/property_tree/ptree_fwd.hpp>
@@ -330,7 +331,8 @@ public:
//BBS: add logic for only difference save
//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);
// 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 void normalize(DynamicPrintConfig &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
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.
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.
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.
size_t size() const { return m_presets.size(); }
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.
template<typename PreferedCondition>
@@ -874,8 +887,10 @@ protected:
// This is a temporary state, which shall be fixed immediately by the following step.
bool select_preset_by_name_strict(const std::string &name);
// Merge one vendor's presets with the other vendor's presets, report duplicates.
std::vector<std::string> merge_presets(PresetCollection &&other, const VendorMap &new_vendors);
// Move the presets of `others` into this collection in one pass. A name this
// 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.
void update_map_alias_to_profile_name();
@@ -890,6 +905,56 @@ protected:
void set_custom_preset_alias(Preset &preset);
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;
// Comparator that sorts "Generic " prefixed presets before others, then alphabetically within each group.
@@ -901,6 +966,10 @@ private:
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.
void sort_presets() {
if (m_type == Preset::TYPE_FILAMENT)
@@ -1054,7 +1123,7 @@ public:
//BBS: change to json format
//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 update_from_preset(const Preset& preset);
File diff suppressed because it is too large Load Diff
+172 -27
View File
@@ -15,6 +15,7 @@
#include <unordered_map>
#include <optional>
#include <array>
#include <atomic>
#include <boost/filesystem/path.hpp>
#include <unordered_set>
@@ -622,36 +623,180 @@ public:
// default_filament_profile must resolve to a system filament.
bool check_printer_default_materials() const;
// Merge one vendor's presets with the other vendor's presets, report duplicates.
// Public so per-vendor-cache consumers (e.g. the setup wizard) can assemble a
// bundle out of several per-vendor caches loaded into separate PresetBundle instances.
std::vector<std::string> merge_presets(PresetBundle &&other);
// One vendor to load, and the directory it is installed in.
struct VendorSource
{
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:
// Load one vendor from the preset cache installed in `dir`, judged against
// the vendor profile there. False, with this bundle left clean, when there
// is no usable cache and the vendor has to be parsed. This is how
// load_vendor_configs_from_json reads a cache.
bool load_vendor_cache(const boost::filesystem::path& dir, const std::string& vendor_name, const PresetBundle* base_bundle);
// Move the presets and vendor profiles of `others` into this bundle, in one pass
// over each collection. A preset whose name this bundle or an earlier one of
// `others` already has is left out and listed under the bundle that repeats it.
std::vector<std::vector<std::string>> merge_presets(const std::vector<PresetBundle*> &others);
// Load one source-form preset entry into this bundle: resolve `inherits`
// and `include`, flatten, validate and register the preset. Returns the
// reason loading failed, empty on success. See the definition for the
// sharing contract between the JSON parse and the cache load.
// retain_configs / retain_includes, when non-null, name the only presets
// registered into config_maps / include_maps (a config copy each). The
// cache load passes the names its entries inherit / include — the only
// ones ever looked up again; the JSON parse retains all, not knowing what
// later subfiles name.
std::string load_vendor_preset(const CachedPreset& entry,
const std::string& path, const std::string& vendor_name,
const PresetBundle* base_bundle,
LoadConfigBundleAttributes flags,
ConfigSubstitutionContext& substitution_context, PresetsConfigSubstitutions& substitutions,
std::map<std::string, DynamicPrintConfig>& config_maps, std::map<std::string, DynamicPrintConfig>& include_maps,
std::map<std::string, std::string>& filament_id_maps,
PresetCollection* presets_collection, size_t& count, bool is_from_lib,
const std::set<std::string>* retain_configs = nullptr, const std::set<std::string>* retain_includes = nullptr);
// What parsing one entry's JSON sub-file reported. Its errors and warnings are
// logged when the entry installs, so they come out in listing order with the
// entry's install errors, as parsing and installing one entry at a time leaves them.
struct EntryParse
{
ConfigSubstitutions substitutions;
// Counted in the bundle's error count.
std::vector<std::string> errors;
std::vector<std::string> warnings;
};
// An EntryParse for each entry of the VendorCacheData list of the same name.
struct VendorParse
{
std::vector<EntryParse> process_entries;
std::vector<EntryParse> filament_entries;
std::vector<EntryParse> machine_entries;
};
// 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.
void clear_printer_hold_aliases();
+6 -28
View File
@@ -400,46 +400,24 @@ bool write_cache_blob(const std::string& path, const std::string& blob)
{
boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size());
// Written beside the target and moved into place, as AppConfig::save does:
// a cache is truncated and rewritten in full, so a write that dies partway
// would otherwise leave a 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";
// Written beside the target and moved into place: a cache is truncated and
// rewritten in full, so a write that dies partway would otherwise leave a
// header claiming more body than the file holds.
try {
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;
fhdr.magic = CACHE_MAGIC;
fhdr.version = CACHE_VERSION;
fhdr.data_size = static_cast<uint64_t>(blob.size());
fhdr.crc32 = crc.checksum();
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
ofs.close(); // flush; close() raises failbit on error
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);
const std::string_view header(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
if (const std::error_code ec = write_file_atomically(path, { header, std::string_view(blob) }, /*binary=*/true)) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << ec.message();
return false;
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
return false;
}
}
+1 -1
View File
@@ -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
// 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
// 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
+1
View File
@@ -378,6 +378,7 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n
|| opt_key == "wipe_tower_bridging"
|| opt_key == "wipe_tower_extra_flow"
|| opt_key == "wipe_tower_no_sparse_layers"
|| opt_key == "wipe_tower_sparse_layers_combination"
|| opt_key == "flush_volumes_matrix"
|| opt_key == "prime_volume"
|| opt_key == "flush_into_infill"
+14
View File
@@ -6705,6 +6705,20 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
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->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.");
+2
View File
@@ -163,6 +163,7 @@ inline bool is_smoothable_infill_pattern(InfillPattern pattern, int multiline =
case ipGrid:
case ipTriangles:
case ipStars:
case ipCubic:
return multiline > 1;
default:
return false;
@@ -1631,6 +1632,7 @@ PRINT_CONFIG_CLASS_DEFINE(
((ConfigOptionString, toolchange_cyclic_order))
((ConfigOptionBool, toolchange_cyclic_first_layer))
((ConfigOptionBool, wipe_tower_no_sparse_layers))
((ConfigOptionBool, wipe_tower_sparse_layers_combination))
((ConfigOptionString, change_filament_gcode))
((ConfigOptionString, change_extrusion_role_gcode))
((ConfigOptionString, process_change_extrusion_role_gcode))
+17
View File
@@ -8,6 +8,8 @@
#include <functional>
#include <type_traits>
#include <system_error>
#include <initializer_list>
#include <string_view>
#include <regex>
#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
// 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);
// 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 {
SUCCESS = 0,
+90 -2
View File
@@ -9,6 +9,8 @@
#include <stdio.h>
#include <filesystem>
#include <sstream>
#include <cerrno>
#include <mutex>
#include <iomanip>
#include <algorithm>
#include <cmath>
@@ -702,13 +704,99 @@ namespace WindowsSupport
std::error_code rename_file(const std::string &from, const std::string &to)
{
#ifdef _WIN32
// Retries and moves an open destination aside itself.
return WindowsSupport::rename(from, to);
#else
boost::nowide::remove(to.c_str());
return std::make_error_code(static_cast<std::errc>(boost::nowide::rename(from.c_str(), to.c_str())));
// rename(2) replaces an existing target atomically; removing it first would
// 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
}
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__
// Copied from boost::filesystem.
// Called by copy_file_linux() in case linux sendfile() API is not supported.
+2
View File
@@ -1046,6 +1046,8 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
// Orca: both tower generators skip sparse layers, so this is not a wipe tower 2 exclusive.
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");
bool have_rib_wall = (wipe_tower_wall_type == WipeTowerWallType::wtwRib)&&have_prime_tower;
+12 -9
View File
@@ -1088,8 +1088,6 @@ wxDEFINE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
wxDEFINE_EVENT(EVT_GLCANVAS_TAB, 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_REDO, 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_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();
#ifdef _WIN32
@@ -7295,15 +7303,10 @@ void GLCanvas3D::_resize(unsigned int w, unsigned int h)
#endif
#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
imgui->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
wxGetApp().imgui()->set_scaling(font_size, m_canvas->GetContentScaleFactor(), 1.0f);
#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
+2 -2
View File
@@ -185,8 +185,6 @@ wxDECLARE_EVENT(EVT_GLCANVAS_MOUSE_DRAGGING_FINISHED, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_UPDATE_BED_SHAPE, SimpleEvent);
wxDECLARE_EVENT(EVT_GLCANVAS_TAB, 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_REDO, 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);
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:
bool _is_shown_on_screen() const;
+29 -14
View File
@@ -85,6 +85,7 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/InstanceLock.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
@@ -857,7 +858,12 @@ void GUI_App::post_init()
slow_bootup = true;
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";
#ifndef __linux__
mainframe->Freeze();
@@ -865,9 +871,6 @@ void GUI_App::post_init()
plater_->canvas3D()->enable_render(false);
mainframe->select_prepare_for_gl_init();
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
if (!plater_->canvas3D()->get_wxglcanvas()->IsShownOnScreen() ||
!plater_->canvas3D()->make_current_for_postinit()) {
@@ -905,8 +908,6 @@ void GUI_App::post_init()
}
if (starts_on_prepare())
mainframe->select_tab(TAB_ID_PREPARE);
else if (is_editor())
mainframe->select_tab(TAB_ID_HOME);
#ifndef __linux__
mainframe->Thaw();
#endif
@@ -3423,6 +3424,10 @@ bool GUI_App::on_init_inner()
}
BOOST_LOG_TRIVIAL(info) << "create the main window";
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 (starts_on_prepare()) {
mainframe->select_tab(TAB_ID_PREPARE);
@@ -3545,7 +3550,7 @@ bool GUI_App::on_init_inner()
update_publish_status();
}
if (m_post_initialized && app_config->dirty())
if (m_post_initialized && app_config->dirty() && app_config->save_due())
app_config->save();
});
@@ -7639,8 +7644,11 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Delete the bundle folder and bundle
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
{
boost::system::error_code ec;
InstanceLock instance_lock(user_presets_lock_path());
boost::filesystem::remove_all(bundle_folder, ec);
}
preset_bundle->bundles.WriteLock();
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());
}
// 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)
{
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();
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
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_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;
preset_file.replace_extension(".json");
// If .json doesn't exist, .info is orphaned
if (!fs::exists(preset_file)) {
// Extract setting_id from .info file
std::string setting_id = extract_setting_id_from_info(info_file.string());
// If .json doesn't exist, .info is orphaned. Read under the lock, so a
// remove_files() in another instance is seen whole or not at all; the
// delete queue's own mutex is taken after the lock is released.
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()) {
// Add to need_delete_presets
delete_preset_from_cloud(setting_id, info_file.string());
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
}
}
}
if (ec)
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
}
+1 -1
View File
@@ -389,7 +389,7 @@ public:
bool is_editor() const { return m_app_mode == EAppMode::Editor; }
bool is_gcode_viewer() const { return m_app_mode == EAppMode::GCodeViewer; }
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 FilterEvent(wxEvent& event) override;
// The Preferences "Default page" choice, stored as its index: 0 Home, 1 Prepare.
+3 -1
View File
@@ -247,7 +247,9 @@ int get_dpi_for_window(const wxWindow *window)
const HDC hdc = GetDC(hwnd);
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__
// TODO
+11 -6
View File
@@ -3,6 +3,7 @@
#include <chrono>
#include <boost/log/trivial.hpp>
#include <wx/evtloop.h>
#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
// overran it.
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.
#ifdef __WXGTK__
// Delay before the next slice; on GTK a due timer runs ahead of repaints and posted events,
// and wxOSX rejects a 0 ms timer.
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.
bool input_pending()
@@ -42,6 +40,13 @@ bool input_pending()
#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
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();
return;
}
if (m_input_idle_ms() < quiet_ms || input_pending()) {
if (m_input_idle_ms() < quiet_ms || input_pending() || yielding()) {
start();
return;
}
+13 -4
View File
@@ -159,8 +159,12 @@ void KBShortcutsDialog::fill_pages()
if (wxGetApp().is_editor()) {
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")),
#endif
});
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->AddStretchSpacer();
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_LEFT_DOWN, [capture](wxMouseEvent&) { capture->SetFocus(); });
sizer->Add(capture, 0, wxLEFT | wxRIGHT | wxEXPAND, FromDIP(20));
@@ -531,10 +538,12 @@ void ShortcutCaptureDialog::record(const KeyChord& chord)
m_ok->Enable(false);
};
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);
} 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 {
m_conflicts = wxGetApp().shortcuts().conflicts(m_shortcut, chord);
m_status->SetForegroundColour(m_status_colour);
+10
View File
@@ -182,6 +182,16 @@ bool KeyChord::is_menu_accelerator() const
return valid() && ((modifiers & (wxMOD_CONTROL | wxMOD_ALT | wxMOD_RAW_CONTROL)) != 0 || (!is_printable(key) && key != WXK_SPACE));
}
// 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
{
if (!valid())
+3
View File
@@ -34,6 +34,9 @@ struct KeyChord
// True when Ctrl or Alt is held or the key is non-printable, the chords a menu can own without
// swallowing typing in text fields.
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.
std::string to_string() const;
+72
View File
@@ -81,6 +81,7 @@
#ifdef __WXGTK__
#include <gtk/gtk.h>
#include <wx/glcanvas.h>
#endif // __WXGTK__
#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));
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
@@ -4008,13 +4012,81 @@ bool MainFrame::Show(bool show)
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
// registered.
void MainFrame::prebuild_pages_when_idle()
{
m_idle.clear();
m_idle.add(m_gl_prebuild);
if (m_param_panel)
m_idle.add(m_param_panel->settings_page_prebuild());
m_idle.add(m_prepare_layout_prebuild);
for (LazyBase* page : m_lazy_pages)
if (page->prebuild_order() >= 0)
m_idle.add(*page);
+32
View File
@@ -140,6 +140,38 @@ class MainFrame : public DPIFrame
wxTimer* m_reset_title_text_colour_timer{ nullptr };
IdleScheduler m_idle;
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.
std::vector<LazyBase*> m_lazy_pages;
// The latest EVT_LOAD_PRINTER_URL, applied when the web Device view is built.
+1 -1
View File
@@ -112,7 +112,7 @@ public:
bool split_multi_line{false};
bool option_label_at_right{false};
// BBS: new layout
wxWindow * stb;
wxWindow * stb{ nullptr };
const wxString icon;
const wxString title;
bool m_labels_hidden{false};
+2 -2
View File
@@ -122,8 +122,8 @@ class ParamsPanel : public wxPanel
wxPanel* m_current_tab { nullptr };
// Builds the selected page's option groups at idle; while no tab is selected yet,
// its first unit selects the default one.
// Builds the selected page's option groups at idle and then shows them for the mode;
// while no tab is selected yet, its first unit selects the default one.
class SettingsPagePrebuild : public LazyBase
{
public:
+122 -120
View File
@@ -799,67 +799,8 @@ void PartPlate::render_logo(bool bottom, bool render_cali)
{
if (!m_partplate_list->render_bedtype_logo) {
// render third-party printer texture logo
if (m_partplate_list->m_logo_texture_filename.empty()) {
m_partplate_list->m_logo_texture.reset();
if (!m_partplate_list->load_logo_texture())
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())
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
void PartPlateList::generate_icon_textures()
{
m_icon_textures_dark = m_is_dark;
// 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;
std::string path = resources_dir() + "/images/";
@@ -4447,6 +4389,9 @@ void PartPlateList::release_icon_textures()
PartPlateList::is_load_bedtype_textures = false;
PartPlateList::is_load_extruder_only_area_textures = 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 (auto& part: bed_texture_info[i].parts) {
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;
}
static bool last_dark_mode_status = m_is_dark;
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();
load_icon_textures();
for (it = m_plate_list.begin(); it != m_plate_list.end(); it++) {
int current_index = (*it)->get_index();
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_extruder_only_area_textures = false; // reload textures
m_next_bedtype_texture = 0;
m_next_extruder_only_area_texture = 0;
calc_bounding_boxes();
update_logo_texture_filename(texture_filename);
@@ -7089,53 +7031,120 @@ bool PartPlateList::init_extruder_only_area_info()
return true;
}
void PartPlateList::load_bedtype_textures()
static GLint logo_texture_size()
{
if (PartPlateList::is_load_bedtype_textures) return;
init_bed_type_info();
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
for (int i = 0; i < (unsigned int)btCount; ++i) {
for (int j = 0; j < bed_texture_info[i].parts.size(); j++) {
std::string filename = resources_dir() + "/images/" + bed_texture_info[i].parts[j].filename;
if (boost::filesystem::exists(filename)) {
PartPlateList::bed_texture_info[i].parts[j].texture = new GLTexture();
if (!PartPlateList::bed_texture_info[i].parts[j].texture->load_from_svg_file(filename, true, true, true, logo_tex_size)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
}
} else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % filename;
}
}
}
PartPlateList::is_load_bedtype_textures = true;
return std::min<GLint>(OpenGLManager::get_gl_info().get_max_tex_size(), 2048);
}
void PartPlateList::load_extruder_only_area_textures() {
if (PartPlateList::is_load_extruder_only_area_textures) return;
auto ok = init_extruder_only_area_info();
if (!ok) {
PartPlateList::is_load_extruder_only_area_textures = true;
return;
// 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;
}
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;
++next;
PartPlateList::BedTextureInfo::TexturePart& part = infos[i].parts[k];
const std::string filename = resources_dir() + "/images/" + part.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;
}
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 logo texture from %1% failed!") % filename;
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load texture from %1% failed!") % filename;
}
return true;
}
return false;
}
void PartPlateList::load_bedtype_textures()
{
while (load_next_bedtype_texture()) {}
}
bool PartPlateList::load_next_bedtype_texture()
{
if (PartPlateList::is_load_bedtype_textures)
return false;
if (m_next_bedtype_texture == 0)
init_bed_type_info();
if (load_next_part_texture(bed_texture_info, btCount, m_next_bedtype_texture, true))
return true;
PartPlateList::is_load_bedtype_textures = true;
return false;
}
bool PartPlateList::load_logo_texture()
{
if (m_logo_texture_filename.empty()) {
m_logo_texture.reset();
return false;
}
if (m_logo_texture.get_id() != 0 && m_logo_texture.get_source() == m_logo_texture_filename) {
if (m_logo_texture.unsent_compressed_data_available())
// sends to gpu the already available compressed levels of the main texture
m_logo_texture.send_compressed_data_to_gpu();
return true;
}
m_logo_texture.reset();
// starts generating the main texture, compression will run asynchronously
if (boost::algorithm::iends_with(m_logo_texture_filename, ".svg")) {
if (!m_logo_texture.load_from_svg_file(m_logo_texture_filename, true, true, true, logo_texture_size())) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename;
return false;
}
}
else if (boost::algorithm::iends_with(m_logo_texture_filename, ".png")) {
if (!m_logo_texture.load_from_file(m_logo_texture_filename, true, GLTexture::MultiThreaded, true)) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": load logo texture from %1% failed!") % m_logo_texture_filename;
return false;
}
}
else {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << boost::format(": can not load logo texture from %1%, unsupported format") % m_logo_texture_filename;
return false;
}
return true;
}
void PartPlateList::load_icon_textures()
{
if (!icon_textures_loaded())
generate_icon_textures();
}
bool PartPlateList::load_next_plate_texture()
{
if (!render_bedtype_logo) {
load_logo_texture();
return false;
}
return load_next_bedtype_texture() || load_next_cali_texture() || load_next_extruder_only_area_texture();
}
void PartPlateList::load_extruder_only_area_textures()
{
while (load_next_extruder_only_area_texture()) {}
}
bool PartPlateList::load_next_extruder_only_area_texture()
{
if (PartPlateList::is_load_extruder_only_area_textures)
return false;
if (m_next_extruder_only_area_texture == 0 && !init_extruder_only_area_info()) {
PartPlateList::is_load_extruder_only_area_textures = true;
return false;
}
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;
return false;
}
void PartPlateList::init_cali_texture_info()
@@ -7150,26 +7159,19 @@ void PartPlateList::init_cali_texture_info()
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();
GLint max_tex_size = OpenGLManager::get_gl_info().get_max_tex_size();
GLint logo_tex_size = (max_tex_size < 2048) ? max_tex_size : 2048;
for (int i = 0; i < (unsigned int)btCount; ++i) {
for (int j = 0; j < 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;
}
}
}
if (load_next_part_texture(&cali_texture_info, 1, m_next_cali_texture, true))
return true;
PartPlateList::is_load_cali_texture = true;
return false;
}
void PartPlateList::on_extruder_count_changed(int extruder_count)
+21
View File
@@ -644,6 +644,7 @@ class PartPlateList : public ObjectBase
std::string m_hover_tooltip;
bool m_is_dark = false;
bool m_icon_textures_dark = false;
int m_filament_count = 1;
@@ -943,12 +944,25 @@ public:
bool calc_extruder_only_area(Rect &left_only_rect, Rect &right_only_rect);
void init_bed_type_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();
bool load_next_bedtype_texture();
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 init_cali_texture_info();
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);
@@ -960,6 +974,13 @@ public:
BedTextureInfo bed_texture_info[btCount];
BedTextureInfo cali_texture_info;
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
+4 -1
View File
@@ -147,9 +147,11 @@ constexpr std::array<ShortcutInfo, size_t(Shortcut::Count)> shortcut_table = {{
SHORTCUT(Search, "search", L("Search"), GLOBAL, { 'F', CTRL }),
SHORTCUT(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(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(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
@@ -177,6 +179,7 @@ constexpr std::array<SectionInfo, size_t(ShortcutSection::Count)> section_table
{ Shortcut::ViewDefault, L("Camera") },
{ Shortcut::ShowLabels, L("Display") },
{ Shortcut::Preferences, L("Application") },
{ Shortcut::SpeedDial, L("Speed Dial") },
}};
constexpr bool sections_follow_table_order()
+4 -2
View File
@@ -47,7 +47,9 @@ enum class Shortcut : uint8_t {
// Display
ShowLabels, ShowWireframe, ToggleGcodeWindow, ToggleOneLayerMode,
// Application
Preferences, Search, SwitchView, CollapseSidebar, SpeedDial, ReloadDevicePage, KeyboardShortcuts,
Preferences, Search, SwitchView, CollapseSidebar, ReloadDevicePage, KeyboardShortcuts,
// Speed Dial
SpeedDial,
Count
};
@@ -66,7 +68,7 @@ struct ShortcutInfo
// Headings of the shortcuts dialog, in listing order.
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
};
+13 -2
View File
@@ -3069,6 +3069,7 @@ void TabPrint::build()
optgroup->append_single_option_line("wipe_tower_rib_width", "multimaterial_settings_prime_tower#rib-width");
optgroup->append_single_option_line("wipe_tower_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_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 = 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
{
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()
{
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()
@@ -8601,6 +8608,8 @@ void Page::update_visibility(ConfigOptionMode mode, bool update_contolls_visibil
}
m_show = ret_val;
if (update_contolls_visibility)
m_visibility_applied = true;
#ifdef __WXMSW__
if (!m_show) return;
// 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];
if (!group->activate(throw_if_canceled))
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
group->update_visibility(mode);
#if HIDE_FIRST_SPLIT_LINE
@@ -8688,6 +8698,7 @@ void Page::clear()
for (auto group : m_optgroups)
group->clear();
m_page_title = NULL;
m_visibility_applied = false;
}
void Page::msw_rescale()
+5 -2
View File
@@ -71,6 +71,7 @@ class Page: public std::enable_shared_from_this<Page>// : public wxScrolledWindo
// BBS: new layout
wxStaticText* m_page_title;
bool m_show = true;
bool m_visibility_applied = false;
public:
//BBS: GUI refactor
Page(wxWindow* parent, const wxString& title, int iconID, wxPanel* tab_owner);
@@ -98,6 +99,8 @@ public:
bool build_pending() const;
// Builds the next option group that has no controls yet; true while some remain.
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 msw_rescale();
void sys_color_changed();
@@ -443,8 +446,8 @@ public:
// BBS: new layout
void set_expanded(bool value);
void restore_last_select_item();
// page_build_pending() says whether the selected page has groups without controls, and
// page_build_step() builds one.
// page_build_pending() says whether the selected page has groups without controls or controls
// not yet shown for the mode, and page_build_step() does the next of those.
bool page_build_pending() const;
bool page_build_step();
+23 -42
View File
@@ -1405,23 +1405,26 @@ bool GuideFrame::BuildProfileDataFromVendors()
// is served from the shipped profiles. Each is stamped by name and
// version alone: a profile change requires a version bump, so those two
// determine content wherever the vendor's copy sits.
struct VendorSource { std::string name; boost::filesystem::path dir; std::string version; };
std::vector<VendorSource> ordered;
auto add_vendor = [&ordered](const std::string& name, const boost::filesystem::path& dir) {
std::vector<PresetBundle::VendorSource> ordered;
json stamps = json::array();
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
// 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.
// A profile without a version (blacklist.json) carries no presets
// and is passed over.
const boost::filesystem::path profile = dir / (name + ".json");
std::string version;
if (boost::filesystem::exists(profile)) {
const Semver v = get_version_from_json(profile.string());
if (v.valid())
ordered.push_back({name, dir, v.to_string()});
if (! v.valid())
return;
version = v.to_string();
} else {
ordered.push_back({name, dir,
VendorCacheFile::peek_version((dir / (name + ".opc")).string(), name)});
version = 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);
if (auto it = vendor_sources.find(filament_library); it != vendor_sources.end())
@@ -1431,9 +1434,6 @@ bool GuideFrame::BuildProfileDataFromVendors()
add_vendor(name, dir);
if (ordered.empty())
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
// 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
// what makes this worth doing instead of the scan below; loading into a
// bundle per vendor keeps the install order the startup path has.
// what makes this worth doing instead of the scan below.
PresetBundle bundle;
auto load_vendor = [](PresetBundle& into, const std::string& vendor,
const boost::filesystem::path& dir, const PresetBundle* base) {
into.load_vendor_configs_from_json(dir.string(), vendor, PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent, base);
};
for (const VendorSource& v : ordered) {
if (*m_cancel_token)
return false; // as in the scan below: a vendor without a cache is parsed, and that takes time
if (v.name == filament_library) {
load_vendor(bundle, v.name, v.dir, nullptr);
} else {
PresetBundle tmp;
load_vendor(tmp, v.name, v.dir, &bundle);
bundle.merge_presets(std::move(tmp));
}
}
if (bundle.vendors.empty())
std::vector<std::string> failed;
const std::string errors = bundle.load_vendors(ordered, ForwardCompatibilitySubstitutionRule::EnableSilent,
/*allow_cache=*/true, m_cancel_token.get(), &failed).second;
if (*m_cancel_token || bundle.vendors.empty())
return false;
if (! errors.empty())
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: loading the vendors reported: " << errors;
// A vendor that failed to load sends this open to the scan below, which lists
// what it can read of every vendor.
if (! failed.empty())
return false;
if (! BuildProfileJson(bundle, /*require_all_resource_vendors=*/false))
return false;
// 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
// instances from interleaving on one temp file.
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
// are: half a cache must never be readable.
try {
json out;
out["format"] = 1;
@@ -1497,18 +1487,9 @@ bool GuideFrame::BuildProfileDataFromVendors()
for (const char* key : { "model", "machine", "filament", "process" })
profile[key] = m_ProfileJson[key];
boost::filesystem::create_directories(cache_file.parent_path());
{
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()))
if (const std::error_code ec = write_file_atomically(cache_file.string(), out.dump(-1, ' ', false, json::error_handler_t::ignore), /*binary=*/true))
throw std::runtime_error(ec.message());
} 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();
}
return true;
+7 -3
View File
@@ -10,6 +10,7 @@
#include <wx/webviewarchivehandler.h>
#include <wx/webviewfshandler.h>
#include <wx/weakref.h>
#if wxUSE_WEBVIEW_EDGE
#include <wx/msw/webview_edge.h>
#elif defined(__WXMAC__)
@@ -235,7 +236,9 @@ class FakeWebView : public wxWebView
wxDEFINE_EVENT(EVT_WEBVIEW_RECREATED, wxCommandEvent);
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
{
@@ -340,8 +343,9 @@ wxWebView* WebView::CreateWebView(wxWindow * parent, wxString const & url)
addScriptMessageHandler(webView);
while (!g_delay_webviews.empty()) {
auto views = std::move(g_delay_webviews);
for (auto wv : views)
addScriptMessageHandler(wv);
for (const wxWeakRef<wxWebView>& wv : views)
if (wv)
addScriptMessageHandler(wv.get());
}
}
#ifndef __WIN32__
+5 -3
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <sstream>
#include <system_error>
#include <exception>
#include <boost/format.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("email", email);
try {
auto temp_path = m_api_session_file_path + ".tmp";
pt::write_json(temp_path, j);
boost::filesystem::rename(temp_path, m_api_session_file_path);
std::ostringstream json;
pt::write_json(json, j);
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) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
<< m_api_session_file_path
+5 -24
View File
@@ -1475,15 +1475,8 @@ void OrcaCloudServiceAgent::save_sync_state()
if (sync_state_path.empty())
return;
try {
std::string tmp_path = sync_state_path + ".tmp";
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 (...) {}
if (const std::error_code ec = write_file_atomically(sync_state_path, std::to_string(sync_state.last_sync_timestamp)))
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to save the sync state: " << ec.message();
}
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);
}
const std::string tmp_path = secret_fallback_path + ".tmp";
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary);
if (ofs.good()) {
ofs << signed_payload;
ofs.flush();
ofs.close();
if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(secret_fallback_path.c_str()), true)) {
if (const std::error_code ec = write_file_atomically(secret_fallback_path, signed_payload, /*binary=*/true))
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot write user secret file " << secret_fallback_path << ": " << ec.message();
else
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 {
// Use wxSecretStore only
wxSecretStore store = wxSecretStore::GetDefault();
+4 -15
View File
@@ -249,21 +249,10 @@ bool PluginConfig::save()
return false;
}
// Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot truncate an
// existing config. Same approach as AppConfig::save().
const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
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();
// Written beside the target and moved into place, so a crash mid-write cannot truncate an
// existing config.
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";
return false;
}
+228
View File
@@ -2,6 +2,7 @@
#include <algorithm>
#include <cmath>
#include <functional>
#include <map>
#include <numeric>
#include <sstream>
@@ -11,8 +12,10 @@
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/AABBTreeLines.hpp"
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp"
#include "libslic3r/Layer.hpp"
#include "libslic3r/Print.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);
}
TEST_CASE("Multiline cubic infill follows the cubic lines without crossing itself", "[Fill]")
{
const int multiline = GENERATE(2, 3);
const double spacing = 0.45;
const double density = 0.3;
const double wall = multiline * spacing;
CAPTURE(multiline);
const ExPolygon region{ Slic3r::Points{ Point::new_scale(0., 0.), Point::new_scale(40., 0.),
Point::new_scale(40., 40.), Point::new_scale(0., 40.) } };
auto fill = [&region, spacing](int lines, double density, size_t layer_id, double z) {
std::unique_ptr<Slic3r::Fill> filler(Slic3r::Fill::new_from_type("cubic"));
filler->spacing = spacing;
filler->angle = float(M_PI / 7.);
filler->layer_id = layer_id;
filler->z = z;
FillParams params;
params.density = float(density);
params.multiline = lines;
params.dont_adjust = true;
params.anchor_length_max = 0.f; // The bare pattern, without connections along the boundary.
Slic3r::Surface surface(stInternal, region);
return filler->fill_surface(&surface, params);
};
// Away from the boundary, where a line is clipped earlier than the side of its wall.
const Polygons inner = shrink(to_polygons(region), scale_(3.));
auto farthest = [&inner](const Polylines &from, const Polylines &to) {
const AABBTreeLines::LinesDistancer<Line> tree(to_lines(to));
double distance = 0.;
for (const Polyline &path : intersection_pl(from, inner))
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
distance = std::max(distance, tree.distance_from_lines<false>(point));
return unscale<double>(distance);
};
// One z period of the pattern: sqrt(2) / 3 of the 3 * wall / density line spacing.
const double z_period = std::sqrt(2.) * wall / density;
const size_t layers = 30;
for (size_t layer_id = 0; layer_id < layers; ++layer_id) {
const double z = z_period * (layer_id + 0.5) / layers;
CAPTURE(layer_id, z);
const Polylines walls = fill(multiline, density, layer_id, z);
REQUIRE_FALSE(walls.empty());
CHECK(get_intersections(to_lines(walls)).empty());
// Long paths running out to the boundary, not loops around the cells.
CHECK(std::none_of(walls.begin(), walls.end(), [](const Polyline &path) { return path.first_point() == path.last_point(); }));
// Single lines at the same spacing: the walls are drawn along them.
const Polylines lines = fill(1, density / multiline, layer_id, z);
REQUIRE_FALSE(lines.empty());
CHECK(farthest(lines, walls) < 0.5 * wall);
CHECK(farthest(walls, lines) < 1.5 * wall);
}
}
TEST_CASE("Multiline adaptive cubic infill keeps its lines apart without closing them around the cells", "[Fill]")
{
const std::string pattern = GENERATE("adaptivecubic", "supportcubic");
const int multiline = GENERATE(2, 3);
CAPTURE(pattern, multiline);
// A sphere refines the octree all around, so the finer lines end on the coarser ones at every layer.
TriangleMesh sphere = Slic3r::Test::mesh(Slic3r::Test::TestMesh::sphere_50mm);
sphere.scale(0.3f);
Print print;
Slic3r::Test::init_and_process_print({sphere}, print,
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "40%"},
{"fill_multiline", multiline},
{"infill_anchor", 0},
{"infill_anchor_max", 0},
{"layer_height", 0.3}});
size_t paths = 0, loops = 0;
for (const Layer *layer : print.objects().front()->layers()) {
Polylines printed;
Polygons sparse;
double spacing = 0.;
for (const LayerRegion *region : layer->regions()) {
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
if (entity->role() == erInternalInfill)
entity->collect_polylines(printed);
for (const Surface &surface : region->fill_surfaces.surfaces)
if (surface.surface_type == stInternal)
append(sparse, shrink(to_polygons(surface.expolygon), scale_(1.)));
spacing = region->flow(frInfill).spacing();
}
if (printed.empty())
continue;
CAPTURE(layer->print_z);
paths += printed.size();
loops += std::count_if(printed.begin(), printed.end(), [](const Polyline &pl) { return pl.first_point() == pl.last_point(); });
CHECK(get_intersections(to_lines(printed)).empty());
// Neighbouring lines stay a line spacing apart, less the overlap of a line end with the wall it stops on.
// Pieces of one line that meet end to end are one line.
std::vector<size_t> line_of(printed.size());
std::iota(line_of.begin(), line_of.end(), 0);
std::function<size_t(size_t)> find = [&](size_t i) { return line_of[i] == i ? i : line_of[i] = find(line_of[i]); };
for (size_t i = 0; i < printed.size(); ++i)
for (size_t j = i + 1; j < printed.size(); ++j)
for (const Point &a : { printed[i].first_point(), printed[i].last_point() })
for (const Point &b : { printed[j].first_point(), printed[j].last_point() })
if ((a - b).cast<double>().norm() < SCALED_EPSILON)
line_of[find(i)] = find(j);
Lines lines;
std::vector<size_t> owner;
for (size_t i = 0; i < printed.size(); ++i)
for (const Line &line : printed[i].lines()) {
lines.push_back(line);
owner.push_back(find(i));
}
AABBTreeLines::LinesDistancer<Line> tree(lines);
double closest = spacing;
for (size_t i = 0; i < printed.size(); ++i)
for (const Point &p : printed[i].equally_spaced_points(scale_(0.1)))
if (contains(sparse, p))
for (size_t k : tree.all_lines_in_radius(p, scale_(spacing)))
if (owner[k] != find(i))
closest = std::min(closest, unscale<double>(lines[k].distance_to(p)));
CHECK(closest > 0.45 * spacing);
}
REQUIRE(paths > 0);
// The lines run on through the cells instead of each cell getting its own loops.
CHECK(loops < paths / 4);
}
TEST_CASE("Multiline adaptive cubic paths touch where they bounce off each other", "[Fill]")
{
const int sweep = GENERATE(0, 1, 2);
// Offset of the third family in walls, so the three meet in points or in small triangles.
const double shift = GENERATE(0., 0.1, 0.5, 1., 2.5, -0.5, -1.);
// Like finer octree lines ending on coarser ones, the 60 degree lines may start on the horizontal line through 0.
const bool starting = GENERATE(false, true);
CAPTURE(sweep, shift, starting);
const double d1 = scale_(0.8), pitch = scale_(8.), inner = scale_(12.);
Lines lines;
for (int k = 0; k < 3; ++k) {
const Vec2d dir(std::cos(k * M_PI / 3.), std::sin(k * M_PI / 3.)), normal(-dir.y(), dir.x());
for (int i = -6; i <= 6; ++i) {
const Vec2d mid = (i * pitch + (k == 2 ? shift * d1 : 0.)) * normal;
const double start = k == 1 && starting ? -mid.y() / dir.y() : -10. * pitch;
lines.emplace_back((mid + start * dir).cast<coord_t>(), (mid + 10. * pitch * dir).cast<coord_t>());
}
}
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, 0., sweep, BoundingBox(Point::new_scale(-20., -20.), Point::new_scale(20., 20.)));
REQUIRE_FALSE(paths.empty());
CHECK(get_intersections(to_lines(paths)).empty());
Lines pieces;
std::vector<size_t> owner;
for (size_t i = 0; i < paths.size(); ++i)
for (const Line &line : paths[i].lines()) {
pieces.push_back(line);
owner.push_back(i);
}
AABBTreeLines::LinesDistancer<Line> tree(pieces);
auto clearance = [&](size_t i) {
const Line &a = pieces[i];
double distance = std::numeric_limits<double>::max();
for (size_t j : tree.all_lines_in_radius(a.midpoint(), 0.5 * a.length() + 2. * d1))
if (owner[j] != owner[i]) {
const Line &b = pieces[j];
distance = std::min({ distance, a.distance_to(b.a), a.distance_to(b.b), b.distance_to(a.a), b.distance_to(a.b) });
}
return distance;
};
auto inside = [inner](const Point &p) { return std::abs(p.x()) < inner && std::abs(p.y()) < inner; };
double closest = std::numeric_limits<double>::max();
for (size_t i = 0; i < pieces.size(); ++i)
if (inside(pieces[i].midpoint()))
closest = std::min(closest, clearance(i));
CHECK(closest > 0.99 * d1);
// Each path at a crossing touches another one there, none stops short of it.
double widest = 0.;
for (size_t i = 0; i < lines.size(); ++i)
for (size_t j = i + 1; j < lines.size(); ++j)
if (Point crossing; line_alg::intersection(lines[i], lines[j], &crossing) && inside(crossing)) {
std::map<size_t, double> at;
for (size_t k : tree.all_lines_in_radius(crossing, 1.2 * d1))
at.emplace(owner[k], std::numeric_limits<double>::max());
for (size_t k : tree.all_lines_in_radius(crossing, 2. * d1))
if (auto it = at.find(owner[k]); it != at.end())
it->second = std::min(it->second, clearance(k));
for (const auto &path : at)
widest = std::max(widest, path.second);
}
CHECK(widest < 1.02 * d1);
}
TEST_CASE("Multiline adaptive cubic paths reach the line they end on when another path ends on them", "[Fill]")
{
const int sweep = GENERATE(0, 1, 2);
// Where the 120 degree line starts on the horizontal one, in walls from the 60 degree line.
const double start = GENERATE(0.3, 0.6, 1., 2.);
CAPTURE(sweep, start);
const double d1 = scale_(0.8), overlap = 0.1 * d1, length = scale_(30.);
const Vec2d diagonal(0.5, 0.5 * std::sqrt(3.)), horizontal(1., 0.), steep(-0.5, 0.5 * std::sqrt(3.));
const Vec2d on_horizontal = start * d1 * horizontal;
const Lines lines{ Line((-length * diagonal).cast<coord_t>(), (length * diagonal).cast<coord_t>()),
Line(Point(0, 0), (length * horizontal).cast<coord_t>()),
Line(on_horizontal.cast<coord_t>(), (on_horizontal - length * steep).cast<coord_t>()) };
const Polylines paths = FillAdaptive::multiline_paths(lines, d1, overlap, sweep, BoundingBox(Point::new_scale(-40., -40.), Point::new_scale(40., 40.)));
// The end of the path along each line nearest to where that line starts.
auto end_along = [&paths](const Line &line) {
for (const Polyline &path : paths)
if (line.distance_to(path.first_point()) < SCALED_EPSILON && line.distance_to(path.last_point()) < SCALED_EPSILON)
return (path.first_point() - line.a).cast<double>().norm() < (path.last_point() - line.a).cast<double>().norm() ? path.first_point() : path.last_point();
return Point(std::numeric_limits<coord_t>::max(), 0);
};
const Point horizontal_end = end_along(lines[1]), steep_end = end_along(lines[2]);
REQUIRE(horizontal_end.x() != std::numeric_limits<coord_t>::max());
REQUIRE(steep_end.x() != std::numeric_limits<coord_t>::max());
// Both reach the overlap into the wall of the path they stop at, none stops short of it.
CHECK_THAT(line_alg::distance_to_infinite(lines[0], horizontal_end) / d1, Catch::Matchers::WithinAbs(0.9, 0.01));
CHECK(lines[1].distance_to(steep_end) / d1 < 0.91);
CHECK(get_intersections(to_lines(paths)).empty());
}
TEST_CASE("3D honeycomb infill rounds its octahedral waves with the smooth factor", "[Fill]")
{
auto shape_for = [](const std::string &smooth_factor) {
+94
View File
@@ -630,3 +630,97 @@ TEST_CASE("A lower layer sliver too thin to print does not support the wall abov
// A rib that does get printed takes the 20mm outer wall running along it out of the overhangs.
CHECK(printable < no_rib - scale_(15.));
}
namespace {
// Every setting the fuzzy skin assertions below depend on.
DynamicPrintConfig fuzzy_skin_config(const char *wall_generator)
{
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
config.set_deserialize_strict({
{ "wall_generator", wall_generator },
{ "layer_height", 0.2 },
{ "initial_layer_print_height", 0.2 },
// One wall, so every wall point along the long sides belongs to the fuzzed outer wall.
{ "wall_loops", 1 },
{ "fuzzy_skin", "external" },
{ "fuzzy_skin_noise_type", "classic" },
{ "fuzzy_skin_thickness", 0.3 },
{ "fuzzy_skin_point_distance", 0.8 },
});
return config;
}
// How far the wall points over the middle 60% of the layer's length stray across its width, worst side.
// A negative result means there is no layer at `print_z`.
double mid_span_wall_spread(const Print &print, double print_z)
{
for (const Layer *layer : print.objects().front()->layers()) {
if (std::abs(layer->print_z - print_z) > 1e-4)
continue;
const BoundingBox bbox = get_extents(layer->lslices);
const coord_t x_min = bbox.min.x() + bbox.size().x() / 5;
const coord_t x_max = bbox.max.x() - bbox.size().x() / 5;
Points points;
for (const LayerRegion *region : layer->regions())
region->perimeters.collect_points(points);
coord_t spread = 0;
for (const bool south : { true, false }) {
coord_t lo = bbox.max.y(), hi = bbox.min.y();
for (const Point &p : points)
if (p.x() > x_min && p.x() < x_max && (p.y() < bbox.center().y()) == south) {
lo = std::min(lo, p.y());
hi = std::max(hi, p.y());
}
spread = std::max(spread, hi - lo);
}
return unscale<double>(spread);
}
return -1.;
}
} // namespace
// TestMesh::bridge is a 50x10mm deck from z=5 to z=8 on two 5mm-wide pillars, leaving a 40mm span. The deck's
// first layer (print_z 5.2) crosses the span unsupported; the layers above it rest on the deck.
TEST_CASE("Fuzzy skin leaves the walls of a bridge smooth", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
Print print;
init_and_process_print({ TestMesh::bridge }, print, fuzzy_skin_config(wall_generator));
REQUIRE_FALSE(print.objects().empty());
// Control: one deck layer up the same walls rest on the deck, so they are fuzzed.
CHECK(mid_span_wall_spread(print, 5.6) > 0.1);
// Over the unsupported span the walls stay straight.
const double bridged = mid_span_wall_spread(print, 5.2);
CHECK(bridged >= 0.);
CHECK(bridged < 0.001);
}
// One object: a 20x20x3mm block on the bed and a second one floating above it from z=5 to z=8. The layers in
// the gap are empty, so the floating block's first layer (print_z 5.2) has a layer below it with nothing
// printed on it; the layers above rest on the floating block.
TEST_CASE("Fuzzy skin leaves the walls over an empty layer smooth", "[Perimeters]")
{
const char *wall_generator = GENERATE("classic", "arachne");
CAPTURE(wall_generator);
TriangleMesh mesh = make_cube(20., 20., 3.);
TriangleMesh floating = make_cube(20., 20., 3.);
floating.translate(0.f, 0.f, 5.f);
mesh.merge(floating);
Print print;
init_and_process_print({ mesh }, print, fuzzy_skin_config(wall_generator));
REQUIRE_FALSE(print.objects().empty());
// Control: one layer up the walls rest on the floating block, so they are fuzzed.
CHECK(mid_span_wall_spread(print, 5.6) > 0.1);
// Nothing is printed under the first floating layer, so its walls stay straight.
const double floating_first_layer = mid_span_wall_spread(print, 5.2);
CHECK(floating_first_layer >= 0.);
CHECK(floating_first_layer < 0.001);
}
+113
View File
@@ -308,6 +308,119 @@ TEST_CASE("A single-filament plate reserves a tower only when one is actually pr
}
}
// Filament 2 on the top surface only, so every layer below it is a toolchange-free tower layer: the
// run "Combine sparse layers" folds. The two heights decide whether anything folds, so they are the
// caller's business.
static DynamicPrintConfig sparse_run_config(double layer_height, const char *max_layer_height, bool combine)
{
DynamicPrintConfig config = multifilament_config(2, {
{ "top_surface_filament_id", 2 },
{ "enable_prime_tower", true },
{ "wipe_tower_x", 50 }, // inside the 200x200 test bed
{ "wipe_tower_y", 50 },
{ "prime_tower_width", 35 },
{ "min_layer_height", "0.08"},
{ "single_extruder_multi_material", true },
{ "timelapse_type", "0" },
{ "enable_wrapping_detection", false },
{ "raft_layers", "0" } });
// A taller first layer would top the plan and hide what the run does, so slice at one height.
config.set_deserialize_strict({ { "layer_height", std::to_string(layer_height) },
{ "initial_layer_print_height", std::to_string(layer_height) },
{ "max_layer_height", max_layer_height },
{ "wipe_tower_sparse_layers_combination", combine ? "1" : "0" } });
return config;
}
// What a sliced tower did with its sparse run.
struct SparseRunResult { size_t planned, sparse, folded; float tallest_printed, printed_height; std::string gcode; };
static SparseRunResult slice_sparse_run(const DynamicPrintConfig &config)
{
Print print;
Model model;
init_print({ cube(10) }, print, model, config);
print.apply(model, config);
print.process();
REQUIRE(print.is_step_done(psWipeTower));
SparseRunResult r{};
for (const std::vector<WipeTower::ToolChangeResult> &layer : print.wipe_tower_data().tool_changes) {
if (layer.empty())
continue;
++r.planned;
if (wipe_tower_layer_is_sparse(layer))
++r.sparse;
if (wipe_tower_layer_is_combined_away(layer)) {
++r.folded;
} else {
r.tallest_printed = std::max(r.tallest_printed, layer.front().layer_height);
r.printed_height += layer.front().layer_height;
}
}
r.gcode = Slic3r::Test::gcode(print);
return r;
}
// How often the G-code declares `height` in the tag this printer's processor reads. The dialect is a
// global the exporter sets from the printer, so this is only correct after a slice - the point below.
static size_t count_height_tags(const std::string &gcode, const char *height)
{
const std::string tag = ";" + GCodeProcessor::reserved_tag(GCodeProcessor::ETags::Height) + height + "\n";
size_t n = 0;
for (size_t p = gcode.find(tag); p != std::string::npos; p = gcode.find(tag, p + 1))
++n;
return n;
}
TEST_CASE("Combining sparse layers folds a run into whole layers the nozzle can lay down", "[WipeTower]")
{
// 0.1 mm layers under a 0.32 mm cap: three fit (0.3), a fourth does not, so a run prints once
// every three layers at 0.3 mm.
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.1, "0.32", false));
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.1, "0.32", true));
REQUIRE(plain.planned == combined.planned); // the plan still has one layer per object layer
REQUIRE(plain.sparse > 10);
CHECK(plain.folded == 0);
CHECK_THAT(plain.tallest_printed, Catch::Matchers::WithinAbs(0.1f, 1e-4f));
CHECK(combined.folded > 0);
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.3f, 1e-4f));
// Two of every three sparse layers fold away, leaving the toolchange layers untouched.
CHECK(combined.folded <= plain.sparse);
CHECK(combined.folded >= plain.sparse / 2);
// What folds away comes back as height on the layer that prints the run: no gap, nothing twice.
CHECK_THAT(combined.printed_height, Catch::Matchers::WithinAbs(plain.printed_height, 1e-3f));
}
TEST_CASE("A run too thin to reach the nozzle's layer height is left alone", "[WipeTower]")
{
// Only whole layers merge, so two 0.2 mm layers (0.4) do not fit a 0.32 mm maximum and the tower
// prints as if the option were off. This is the common 0.4 nozzle case; the tooltip says so.
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.32", false));
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.32", true));
REQUIRE(plain.sparse > 10);
CHECK(combined.folded == 0);
CHECK(combined.planned == plain.planned);
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.2f, 1e-4f));
}
TEST_CASE("A merged tower layer declares its own height to the G-code processor", "[WipeTower]")
{
// Each writer declares a height in a hardcoded tag dialect while the processor reads only its
// printer's, so one of them is always dropped. A merged layer is the first time that shows, as a
// thick layer drawn and costed as a thin one. 0.2 mm layers under a 0.42 mm maximum merge in pairs.
const SparseRunResult plain = slice_sparse_run(sparse_run_config(0.2, "0.42", false));
const SparseRunResult combined = slice_sparse_run(sparse_run_config(0.2, "0.42", true));
REQUIRE(combined.folded > 0);
CHECK_THAT(combined.tallest_printed, Catch::Matchers::WithinAbs(0.4f, 1e-4f));
// Every layer that prints a merged run has to say so, and nothing may say so without the option.
CHECK(count_height_tags(combined.gcode, "0.4") - count_height_tags(plain.gcode, "0.4") == combined.folded);
}
TEST_CASE("A tower printed without a tool change is still validated against the bed", "[WipeTower]")
{
// Wrapping detection prints a tower on a plate that purges one filament. Neither the old
+3
View File
@@ -14,7 +14,9 @@ add_executable(${_TEST_NAME}_tests
test_clipper_utils.cpp
test_config.cpp
test_config_variant_expansion.cpp
test_locales_utils.cpp
test_toolordering_nozzle_group.cpp
test_parallel_resolve.cpp
test_preset_bundle_loading.cpp
test_preset_setting_id.cpp
test_preset_diff.cpp
@@ -49,6 +51,7 @@ add_executable(${_TEST_NAME}_tests
test_ordering_strategies.cpp
# test_png_io.cpp
test_indexed_triangle_set.cpp
test_instance_lock.cpp
../libnest2d/printer_parts.cpp
)
+204
View File
@@ -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
+69
View File
@@ -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());
}
}
+122
View File
@@ -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 "libslic3r/PresetBundle.hpp"
#include "libslic3r/ParallelResolve.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -17,6 +18,10 @@
#include <iostream>
#include <initializer_list>
#ifndef _WIN32
#include <unistd.h> // geteuid
#endif
using namespace Slic3r;
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");
}
// 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).
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)));
}
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]")
{
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"));
}
}
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
+109
View File
@@ -10,6 +10,8 @@
#include <cctype>
#include <fstream>
#include <string>
#include <thread>
#include <system_error>
#ifndef _WIN32
#include <unistd.h> // getuid
@@ -62,6 +64,113 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
#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]") {
ScopedTemporaryFile source(".txt");
{
+480 -18
View File
@@ -3,12 +3,21 @@
#include <boost/filesystem.hpp>
#include <boost/crc.hpp>
#include <cereal/archives/binary.hpp>
#include <algorithm>
#include <atomic>
#include <cstring>
#include <fstream>
#include <functional>
#include <memory>
#include <numeric>
#include <random>
#include <set>
#include <sstream>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <tbb/task_group.h>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PresetCacheFormat.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"})";
}
// 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
// base filament with an instantiated child that inherits it and includes a
// 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"("})";
}
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
// 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)
@@ -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
}
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]")
{
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));
}
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]")
{
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);
CHECK(presets_loaded == 1);
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]")
@@ -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)));
}
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]")
{
// 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.});
}
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]")
{
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(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);
}
+92
View File
@@ -278,6 +278,98 @@ TEST_CASE("Only the keep-out ring an object is measured against is drawn", "[Wip
CHECK_THAT(unscaled(get_extents(zone.grown_body).max.x()), WithinAbs(10. + 0.5 * (40. - 0.2), 0.02));
}
// ---------------------------------------------------------------------------------------------
// "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]") {
// 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.
+14
View File
@@ -120,6 +120,20 @@ TEST_CASE("Only modified or non-printable chords qualify as menu accelerators",
CHECK(registry.accelerator(Shortcut::KeyboardShortcuts).empty());
}
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]")
{
const wxAcceleratorEntry entry = KeyChord{ 'S', wxMOD_CONTROL | wxMOD_SHIFT }.to_accelerator_entry(42);