Compare commits

..
Author SHA1 Message Date
Ian Chua e9d1bb85b8 fix: shim python create thread to audit threads 2026-10-20 13:49:07 +08:00
Florian Benkö c9d897b4d0 Fix send dialog crash for a single-nozzle plate on a two-extruder printer (#16599)
update_material_item_pos() chose the left or right nozzle panel from the printer alone and then read m_filaments_map, which reset_and_sync_ams_list() fills only for a multi-nozzle plate. Clicking Print plate with a plate sliced for one nozzle (A1) and a two-extruder printer selected (X2D) read the empty map and crashed. For a single-nozzle plate the dialog now uses the single panel.
2026-10-19 20:52:15 -05:00
Mihai Mocanu 6bff3c3cd6 Fix build_linux.sh distro detection when os-release uses single quotes (#16566)
Gentoo writes `ID='gentoo'` to /etc/os-release. The script only stripped double quotes, so `-u` looked for `scripts/linux.d/'gentoo'` and exited. Single quotes are now stripped too, for both `ID` and `ID_LIKE`.

Fixes #16567
2026-10-19 20:49:37 -05:00
S H 6e1d15f8a2 Avoid signed-shift undefined behavior in EdgeGrid distance propagation (#16626)
Danielsson propagation in EdgeGrid::Grid::calculate_sdf() doubles a negative neighbor offset (-1 or -ncols) with a left shift, which is undefined behavior in C++17. Multiply by two instead.

Adds test_edge_grid.cpp, which checks signed distances inside and outside a rectangle beyond the initial edge band. Built with -fsanitize=shift, it aborts on the old code and passes with the fix.
2026-10-19 20:44:24 -05:00
Colin Trachte 4f63cd4051 Add Reload from disk to multi-selection menu (#16589)
Show Reload from disk in the right-click menu when several objects or parts are selected, as it already is for a single object or part. reload_from_disk() already handles selections across several objects, the same way Reload All uses it.

Fixes #10349
2026-10-19 20:43:19 -05:00
Downtime c7489d431d fix(HMS): load packaged data for additional printer families (#16267)
Add 20P, 26A and 31B to package_dev_id_types so Orca loads the HMS files it already ships for them. Before this, HMS errors on these printers fell through to the cloud lookup and could show as "Unknown error."
2026-10-19 20:40:17 -05:00
HanifKoh 506dd497db Translate H2C Nozzle Rack Filament IDs at the Printer Boundary (#16612)
The Bambu payload rewrite only matched tray_info_idx, filament_id and filamentId. The H2C nozzle rack puts filament ids in cate (the auto nozzle mapping request) and fila_id (the rack status), so the printer got Orca's OF ids for the filaments and its own ids back for the nozzles in the same request, and the device tab could not name a rack nozzle's filament. Add both keys to one shared list used by the rewrite and its pre-check.
2026-10-19 19:42:05 -05:00
HanifKoh 56da2f43b0 Remove the Unused Slice Preset Record That Raced the Slicing Thread (#16649)
`Plater::record_slice_preset` built a json from the current presets and then discarded it. It also read `full_print_config()` on the UI thread while the slicing thread's filament-map write-back reassigned it. On multi-filament projects this crashed Orca right after a slice started.

Remove the function and its two callers in `Plater::reslice()` and `PrintJob::process`.
2026-10-19 19:40:09 -05:00
yw4z 00838d59c1 Remove unused NotoSansKR fonts and fix the emboss fallback font (#16665)
Remove `NotoSansKR-Regular.ttf` and `NotoSansKR-Bold.ttf` from `resources/fonts` (12.4 MB). No code loads them. Korean UI text uses NanumGothic. The files came back by accident with the BambuStudio 1.8 merge (#2844), and macOS and the Flatpak still registered them as app fonts.

Emboss text that uses "Noto Sans KR" on macOS or Flatpak now shows the "Can't load exactly same font" warning when edited.

The emboss tool's fallback for a system with no usable TTF font pointed at `NotoSans-Regular.ttf`, which Orca does not ship. It now uses `HarmonyOS_Sans_SC_Regular.ttf`.
2026-10-19 19:36:54 -05:00
Downtime 8f8bfd6a28 fix: align custom gcode placeholder metadata (#16272)
Declare the runtime placeholders that filament, layer-change, timelapse, wrapping-detection, machine-end and change-filament G-code receive, so the Edit G-code dialog can list them and a build with ORCA_CHECK_GCODE_PLACEHOLDERS no longer rejects them. Adds the missing toolchange_z definition and wraps the new labels and tooltips in L() so they get translated.
2026-10-19 19:20:51 -05:00
Robert J AudasandRF47 90a5a0b964 Fix JPEG texture imports and filament-matching crash (#16607)
Co-authored-by: RF47 <162915171+RF47@users.noreply.github.com>
2026-10-19 20:30:03 -03:00
Kris Austin f64beb4379 Fix random crash while slicing with the Russian UI (daily tips) (#16664)
Fix crash when a daily tip has no body text

has_cjk() looped while i < text.size() - 2. For a string shorter than
two bytes the size_t subtraction wraps, so on an empty string the loop
read past the buffer and up the stack until it hit an unmapped page.

The Russian translation of the "Reverse on even" tip had lost the \n
after its title, so render_text() passed an empty body to has_cjk()
every time that tip was shown during slicing. Restore the line break
and bound the loop so a translation like this can't crash the app.
2026-10-19 16:25:07 -05:00
Kris Austin a2b1a2f3e7 Keep an offline Moonraker printer from freezing startup (#16668)
Moonraker discovery asked the printer for its name before announcing
it, on the GUI thread, with a 5 s connect timeout. When the selected
printer was off, the window ignored input for 5 s after every launch
and the Home page stayed white.

The request never produced a name. It read machine_name or hostname
from /server/info, and Moonraker sends neither there. Discovery now
announces the preset model name without asking the printer.
2026-10-19 16:21:59 -05:00
yw4z 0585836550 Fetch cloud plugin metadata faster (#16645)
When logged in to Orca Cloud, the plugin fetch at startup no longer refreshes the token a second time while start() is already refreshing it. It now waits for that refresh and checks the token again. The subscribed and owned plugin requests also run in parallel, so the fetch takes about half as long.
2026-10-19 15:56:52 -05:00
5a36120c58 fix(gcode): use Klipper motion planning for time estimates (#15545)
Co-authored-by: Ian Bassi <ian.bassi@outlook.com>
Co-authored-by: raistlin7447 <kris.austin@gmail.com>
2026-10-19 16:15:59 -03:00
Ian BassiandTobias Gloth 69193d5681 Optimize travel: exact/refined path chaining, object tours, and joined infill lines (#16492)
* Shorten the travel between extrusion paths

chain_and_reorder_extrusion_entities() builds the print order of the infill,
ironing and support paths of a region from the nozzle position. It took the
greedy chain, which leaves its longest joins for last; it now shortens it:

- Up to eight paths, chain_segments_exact() finds the shortest chain over
  every order and allowed reversal, by dynamic programming over the subsets
  of paths, with a fixed start and a free end.
- Longer chains are refined by local search: 2-opt, or-opt of up to three
  paths, and moving a run of any length next to the nozzle to the front, each
  tried only toward the 32 end points nearest a link, which come from a
  uniform grid. A move is applied only if it shortens the chain, so the result
  is never longer than the greedy one.
- A nearest-next chain is built too, and refined and used instead when it is
  already shorter than the refined greedy chain, which happens when the greedy
  chain sweeps away from the nozzle and has to come back with a long jump.

The shared greedy chaining also chains two to four segments exactly, which
covers the islands of a layer and the loops of a perimeter collection. Paths
that cannot reverse, such as Grid's, are only reordered, and collections that
keep their order are left alone. The overload without a start point keeps the
greedy chain.

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>

* Order objects and islands along shorter tours

Objects under print_order Default, and the tour of islands across objects, are
visited in the same order on every layer, so chain_points_with_postprocessing()
orders them as a closed tour. Up to ten points, tsp_exact_cycle() finds the
shortest tour by dynamic programming over subsets, starting at the point
nearest the start point when one is given. Larger sets add or-opt to the
existing 2-opt and crossing removal: runs of up to three points move next to
one of the eight points nearest an end of the run, reversed or not, and
crossing removal runs last so no crossing is left. Snake ordering is
unchanged; Best of strategies gets the shorter Default tour as a candidate.

* Join infill lines into fewer paths

Every separate infill path costs a travel to it, so the fill generators now
leave fewer of them:

- connect_infill() pairs the line ends along each wall in the order that
  joins the most lines: besides shortest arc first, it replays arcs of about
  the same length taken along each contour, forwards and backwards, including
  the last pass that joins neighboring open ends, and keeps the order that
  joins the most. Regular patterns no longer leave loose ends where the
  pairs along a wall fall out of step, and multiline copies print out and back.
- Ends whose anchors would meet are joined along the wall instead, up to two
  anchors and a line width apart.
- An unpaired end anchors toward the nearest open end of another path, where
  the next path usually starts, instead of the side with more free wall. With
  multiline infill, an anchor toward a copy of the same line stops short of
  the copy instead of printing over it.
- Last, sparse infill lines whose ends are at most two line spacings apart are
  joined with a straight piece, shortest first, without closing a loop or
  crossing a line; with anchors off this is the only joining. Copies of
  neighboring multiline groups are joined at the same side, and lines that
  cannot reverse only join end to start.

* Describe the travel optimization design

How the extrusion chains, the object and island tours and the infill line joining shorten the travel, and why each part is built the way it is.

---------

Co-authored-by: Tobias Gloth <tobias.gloth@3dsystems.com>
2026-10-19 15:53:32 -03:00
Kris Austin cdb9bb57ae fix: Mesh Boolean crash on Windows builds made with clang-cl (#16651)
Build without precompiled headers when the compiler is clang-cl, in both the slicer and the wxWidgets dependency, and add a CI check that fails if broken exception throw info comes back.

With a precompiled header, clang-cl can leave the copy constructor out of the throw info it emits for an exception type (llvm/llvm-project#53486, open since 2022 and still present in clang 22.1.3). The linker keeps one throw info per type for the whole binary. When it keeps one from a precompiled header file, a copy of that exception is bitwise, and the copy and the original free the same data.

Mesh Boolean hits this on every operation. mcut destroys a std::promise that was never set, which stores a broken_promise future_error in an exception_ptr. Freeing it corrupts the heap, and Windows ends the process with 0xC0000374. The 2.5.0-alpha release has broken throw info for 64 exception classes, including std::runtime_error, std::bad_alloc and boost exceptions.

scripts/check_exception_throw_info.ps1 runs after the Windows clang build and its uploads, before any deploy. It fails when an exception class has throw info both with and without a copy constructor, when OrcaSlicer.pdb is missing, or when it finds no exception classes at all. scripts/test_check_exception_throw_info.ps1 tests it on canned dumps.

build_win.bat now treats -l as --no-pch. The Windows ccache key drops the cl.exe version, which only mattered for precompiled headers.

The bug came in with #15428, which moved Windows CI to clang-cl. 2.4.2 was built with MSVC.

Fixes #16581
2026-10-19 12:13:28 -05:00
Sergei f0c59208e7 Apply post-processing scripts to G-code packed into .gcode.3mf (non-BBL) (#16464)
Post-processing scripts now run on a copy of each sliced plate's G-code before it is packed into a .gcode.3mf. This covers Export plate sliced file, sending a .3mf to a print host, and the print dialog for non-BBL printers. BBL printers are skipped, because their G-code is post-processed in place after slicing. Backups are skipped too.

Export plate sliced file no longer reports success when export_3mf() fails.
2026-10-19 10:47:03 -05:00
Ian Bassi 9f6a344162 Make the adaptive infill settings pattern-neutral (#16654) 2026-10-19 12:05:55 -03:00
93 changed files with 4183 additions and 1784 deletions

No files matched your search

+8
View File
@@ -19,6 +19,8 @@ on:
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
@@ -41,6 +43,8 @@ on:
- 'build_linux.sh'
- 'build_win.bat'
- 'scripts/test_build_win.ps1'
- 'scripts/check_exception_throw_info.ps1'
- 'scripts/test_check_exception_throw_info.ps1'
- 'build_release_macos.sh'
- 'scripts/build_preset_cache.*'
- 'scripts/flatpak/**'
@@ -84,6 +88,10 @@ jobs:
shell: powershell
run: .\scripts\test_build_win.ps1
- name: Run the exception throw info check tests
shell: powershell
run: .\scripts\test_check_exception_throw_info.ps1
build_linux:
strategy:
fail-fast: false
+7 -9
View File
@@ -65,15 +65,6 @@ jobs:
shell: bash
run: |
leg="${{ runner.os }}-${{ inputs.arch || 'amd64' }}${{ runner.os == 'Windows' && format('-{0}', inputs.compiler) || '' }}"
# clang-cl refuses a precompiled header from another cl.exe build and ccache
# does not hash that build, so each one gets its own cache. The build number
# is read from cl.exe itself; the toolset directory keeps its name across patches.
if [ "${{ runner.os }}" = Windows ]; then
vswhere='/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe'
toolset=$(tr -d '\r\n' < "$("$vswhere" -latest -products '*' -find 'VC\Auxiliary\Build\Microsoft.VCToolsVersion.default.txt' | tr -d '\r')")
cl=$("$vswhere" -latest -products '*' -find 'VC\Tools\MSVC\'"$toolset"'\**\cl.exe' | tr -d '\r' | head -1)
leg="$leg-vc$("$cl" 2>&1 | grep -o -E 'Version [0-9.]+' | cut -d' ' -f2)"
fi
echo "CCACHE_LEG=$leg" >> "$GITHUB_ENV"
echo "CCACHE_ENTRY=ccache-$leg-${{ github.run_id }}-${{ github.run_attempt }}" >> "$GITHUB_ENV"
@@ -552,6 +543,13 @@ jobs:
name: OrcaSlicer_profile_validator_Windows_${{ env.ver }}
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Check exception throw info Win
if: runner.os == 'Windows' && inputs.compiler == 'clang'
working-directory: ${{ github.workspace }}
# After the uploads, so a failure still leaves the build and its PDB to inspect, and before any deploy.
shell: pwsh
run: scripts\check_exception_throw_info.ps1 -BuildDir $env:BUILD_DIR
- name: Deploy Windows release portable
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: ./.github/actions/deploy-nightly
+8
View File
@@ -321,6 +321,14 @@ if (IS_CROSS_COMPILE)
set(BUILD_TESTS OFF CACHE BOOL "" FORCE)
endif ()
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
if (MSVC AND CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
message(STATUS "clang-cl: building without the precompiled header (llvm/llvm-project#53486)")
set_property(CACHE SLIC3R_PCH PROPERTY HELPSTRING "Use precompiled headers (ignored with clang-cl, llvm/llvm-project#53486)")
set(SLIC3R_PCH OFF)
endif ()
# Print out the SLIC3R_* cache options
get_cmake_property(_cache_vars CACHE_VARIABLES)
list (SORT _cache_vars)
+2 -2
View File
@@ -473,8 +473,8 @@ fi
# cmake 4.x compatibility workaround
export CMAKE_POLICY_VERSION_MINIMUM=3.5
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d '"')
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d '"')
DISTRIBUTION=$(awk -F= '/^ID=/ {print $2}' /etc/os-release | tr -d "'\"")
DISTRIBUTION_LIKE=$(awk -F= '/^ID_LIKE=/ {print $2}' /etc/os-release | tr -d "'\"")
# Check for direct distribution match to Ubuntu/Debian
if [ "${DISTRIBUTION}" == "ubuntu" ] || [ "${DISTRIBUTION}" == "linuxmint" ] ; then
DISTRIBUTION="debian"
+7 -4
View File
@@ -53,7 +53,7 @@ call :add_section "Build configuration"
call :add_arg config string "" config "release, debug, relwithdebinfo or minsizerel (default: release)"
call :add_arg target_arch string "" arch "x64 or arm64 (default: the host architecture)"
call :add_arg slicer_asan bool a asan "Build the slicer with ASAN enabled"
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by --cache"
call :add_arg no_pch bool "" no-pch "Build without the precompiled header, implied by -l and --cache"
call :add_section "Toolchain"
call :add_arg use_clang_cl bool l clang-cl "Use clang-cl as the compiler"
@@ -388,6 +388,9 @@ if "%use_ninja%" == "ON" if "%use_msbuild%" == "ON" (
exit /b 1
)
REM CMakeLists turns the precompiled header off for clang-cl (llvm/llvm-project#53486).
if "%use_clang_cl%" == "ON" set "no_pch=ON"
set "generator=!vs_gen_%vs_version%!"
if "%use_ninja%" == "ON" (
set "generator=Ninja Multi-Config"
@@ -543,7 +546,7 @@ REM real path rather than one glued onto the repository root.
for %%p in ("!build_dir!") do set "build_full=%%~fp"
echo Configuration: %build_type%, %arch%
if not "%clang_exe%" == "" echo Compiler: %clang_exe%
if "%no_pch%" == "ON" echo Precompiled header: off
if "%build_slicer%" == "ON" if "%no_pch%" == "ON" echo Precompiled header: off
if not "%cache_args%" == "" echo Compiler cache: %cache_exe%
set "SIG_FLAG="
@@ -716,8 +719,8 @@ if "%build_slicer%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_ASAN=ON"
)
REM A later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins, because
REM CMake takes the last definition on the command line.
REM Under cl, a later -DSLIC3R_PCH=ON in ORCA_SLICER_CMAKE_ARGS still wins,
REM because CMake takes the last definition on the command line.
if "%no_pch%" == "ON" (
set "slicer_args=!slicer_args! -DSLIC3R_PCH=OFF"
)
+5 -2
View File
@@ -26,6 +26,7 @@ orcaslicer_add_cmake_project(OpenCV
URL https://github.com/opencv/opencv/archive/refs/tags/4.6.0.tar.gz
URL_HASH SHA256=1ec1cba65f9f20fe5a41fda1586e01c70ea0c9a6d7b67c9e13edf0cfe2239277
PATCH_COMMAND git apply ${OpenCV_DIRECTORY_FLAG} --verbose --ignore-space-change --whitespace=fix ${CMAKE_CURRENT_LIST_DIR}/0001-vs.patch ${CMAKE_CURRENT_LIST_DIR}/0002-clang19-macos.patch
DEPENDS ${JPEG_PKG}
CMAKE_ARGS
-DBUILD_SHARED_LIBS=0
-DBUILD_PERE_TESTS=OFF
@@ -34,7 +35,8 @@ orcaslicer_add_cmake_project(OpenCV
-DBUILD_EXAMPLES=OFF
-DBUILD_JASPER=OFF
-DBUILD_JAVA=OFF
-DBUILD_JPEG=ON
# Share Orca's libjpeg to avoid conflicting codec symbols in the application.
-DBUILD_JPEG=OFF
-DBUILD_APPS_LIST=version
-DBUILD_opencv_apps=OFF
-DBUILD_opencv_java=OFF
@@ -67,7 +69,8 @@ orcaslicer_add_cmake_project(OpenCV
-DWITH_OPENJPEG=OFF
-DWITH_QUIRC=OFF
-DWITH_VTK=OFF
-DWITH_JPEG=OFF
# Textured GLB/OBJ imports decode embedded and external JPEGs with cv::imdecode.
-DWITH_JPEG=ON
-DWITH_WEBP=OFF
-DWITH_TIFF=OFF
-DBUILD_TIFF=OFF
+9 -1
View File
@@ -21,6 +21,14 @@ else ()
set(_wx_edge "-DwxUSE_WEBVIEW_EDGE=OFF")
endif ()
# With a precompiled header, clang-cl can omit an exception type's copy constructor from its throw info,
# so copies of the exception share and double-free their data: https://github.com/llvm/llvm-project/issues/53486
if (MSVC AND CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(_wx_precomp OFF)
else ()
set(_wx_precomp ON)
endif ()
set(_wx_patch_command "")
if (APPLE)
set(_wx_patch_command
@@ -39,7 +47,7 @@ orcaslicer_add_cmake_project(
PATCH_COMMAND ${_wx_patch_command}
DEPENDS ${PNG_PKG} ${ZLIB_PKG} ${EXPAT_PKG} ${JPEG_PKG}
CMAKE_ARGS
-DwxBUILD_PRECOMP=ON
-DwxBUILD_PRECOMP=${_wx_precomp}
${_wx_toolkit}
"-DCMAKE_DEBUG_POSTFIX:STRING=${_wx_debug_postfix}"
-DwxBUILD_DEBUG_LEVEL=0
@@ -1,8 +1,8 @@
# Adaptive TPMS infill — High Level Design
# Adaptive infill — High Level Design
## Purpose and scope
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
`adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density
`sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
the density at the center, and `adaptive_infill_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it.
@@ -22,9 +22,9 @@ split into shells or blended between densities.
## Radial field
`TpmsRadialField` gives every point of an object the center of its lobe and a
`AdaptiveInfillField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the
@@ -202,7 +202,7 @@ built for them.
## Constraints
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
- With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large.
+5 -2
View File
@@ -350,8 +350,11 @@ identity default and translates nothing. `NetworkAgent` forwards both to the liv
comparison sites below reach them through `wxGetApp().getAgent()` and leave an id untranslated
while no agent is live. Whole documents are Bambu's business alone:
`BBLPrinterAgent::to_orca_payload` and `from_orca_payload` rewrite every string under
`tray_info_idx`, `filament_id` or `filamentId` at any depth; text that does not parse, or carries
none of those keys, comes back unchanged. The map is loaded once, lazily; a missing or malformed
`tray_info_idx`, `filament_id`, `filamentId`, `cate` or `fila_id` at any depth; text that does not
parse, or carries none of those keys, comes back unchanged. The last two are the H2C nozzle rack's:
`cate` in the auto nozzle mapping request, `fila_id` in the rack status. A new Bambu message that
carries a filament id under another key needs that key added to `filament_id_keys`, or the id
crosses the boundary untranslated. The map is loaded once, lazily; a missing or malformed
file degrades to identity with a log line rather than failing.
| Boundary | Where it translates |
+198
View File
@@ -0,0 +1,198 @@
# Travel optimization — High Level Design
## Purpose and scope
Inside an island, the infill paths of each region are chained into one print
order by a greedy pass that joins the closest free path ends first. That pass
leaves its longest joins for last, and they often cross the whole island. Two
things shorten the travel between infill paths:
- the fill generators join their lines into as few paths as they can, since
every separate path needs a travel to it;
- the G-code export refines the greedy chain with a local search that shortens
the straight-line travel between the paths.
The refinement covers every chain that `chain_and_reorder_extrusion_entities()`
builds from a start point: the infill and ironing of `GCode::extrude_infill()`
and the support of `GCode::extrude_support()`, as well as the order of objects
and islands under `print_order` Default. It changes only the order and the
direction of the paths, not the paths or the walls. Neither part has a setting.
## The chain
`ObjectByExtruder::Island::Region::append()` flattens every sortable fill
collection into its paths, so the paths of all the fills of a region in one
island are chained together. A collection that must keep its order (`no_sort`:
monotonic, concentric, a forced fill order, the flow calibration) stays a single
entry that cannot be reversed. Paths that must keep their direction cannot be
reversed either: Grid's, since it alternates their direction between layers for
strength, and those of a fill with a forced order. A loop starts and ends at the
same point, so reversing it changes nothing.
`extrude_infill()` chains each region from the current nozzle position with
`chain_and_reorder_extrusion_entities()`, and chains the paths inside a nested
sortable collection the same way; `extrude_support()` chains the support paths
of a layer so too, unless they keep their order. The overload without a start
point, used to chain collections while slicing, keeps the greedy chain. The
order the fill generators give their paths does not reach the G-code for a
sortable fill, which is chained again.
## Connecting the lines
Most patterns cut their lines at the infill boundary, and `Fill::connect_infill()`
joins pairs of line ends that are neighbors on a boundary contour by the arc of
wall between them, shortest arcs first, as long as neither end is joined yet and
the join closes no loop. An arc is joined when it is shorter than
`infill_anchor_max`, or than two anchors and a line width if that is longer:
two ends further apart each get an anchor toward the other, and where those
would meet, the join prints about as much wall as one path instead of two.
Regular patterns leave many arcs of the same length, and the order among them
decides the pairing. In an arbitrary order the pairs along one wall can fall out
of step, such as lines 1–2 and 3–4, then 6–7 and 8–9, which leaves the end of
line 5 loose, and every loose end starts or ends a separate path. So the arcs
are also sorted in steps of half the line spacing and, within a step, along each
contour forwards and backwards, which pairs the ends of a wall in one phase.
Each order is replayed on the flags and a union-find of the lines, without
building any path, and the one that joins the most lines is used; a tie keeps
the shortest-first order.
The replay includes the last pass, which joins any two open ends that are
neighbors on a contour. With multiline infill the main pass leaves the copies of
one line to that pass, so that each copy reaches a wall of its own. Two
neighboring groups cut at both walls can then be joined at either wall by arcs
of the same length: at one, their open ends stay next to their own copies, which
the last pass joins, so each line prints out and back; at the other, the copies
end up in separate paths printed apart.
A line end left unpaired gets an anchor, a piece of the wall at most
`infill_anchor` long. It runs toward the nearest open end of another path,
looking up to 64 ends along the contour, since the next path usually starts
there; without one it takes the side with more free wall. With multiline infill
an anchor toward a copy of the same line stops short of the copy rather than
printing over it. A copy is a neighboring end less than the group width
(`multiline` spacings) across the line, at any wall angle, while the next group
lies a whole line spacing away. Meeting the wall at an angle θ, the copy covers
half a width over sin θ of the wall on each side of its end, so the anchor stops
that far and half its own width before it, or is left out; the end still counts
as anchored, so the last pass pairs the other ends as before.
Last, `Fill::fill_surface_extrusion()` joins the sparse infill lines whose ends
are at most two line spacings apart with a straight piece. With the anchors off
(`infill_anchor_max` 0) nothing above runs and this is the only joining; with
them on, it takes what the steps above leave next to each other, such as two
anchors that stop just short of each other or a short piece cut off by a corner.
A gap that short leaves no room to print anything in between, and as a travel it
is the one hop a reordering can trade for a longer travel that retracts.
Multiline copies leave the most such ends: one spacing apart across the line,
their ends where it meets a wall at an angle θ are one spacing over sin θ apart
along it, so two spacings reach walls met at 30° and steeper.
Ends that meet are joined first, then the gaps between the leading ends of their
lines, along each line's overall direction, then the rest, shortest first within
each, without closing a loop or crossing a line or another join. Taking the
leading ends first joins every pair of copies at the same side, which leaves
their open ends next to each other; by length alone, neighboring pairs were
joined at different walls and the travel between them crossed the island. Lines
that cannot reverse, such as Grid's, only join the end of one to the start of the
next, and fills that keep their order are left alone.
## The search
A chain of up to eight paths is found exactly, by dynamic programming over the
subsets of paths: a state is the paths visited so far, the last one and its
direction, about four thousand states for eight paths. The shared greedy
chaining, `chain_segments_greedy_constrained_reversals_()`, does the same for two
to four segments, which also orders up to four islands of a layer and the loops
of a perimeter collection exactly. Either way the first path is no longer
always the one nearest the start; with a free end, a path near the start can be
worth leaving for last.
Objects (`print_order` Default) and the tour of islands across objects are
visited in the same order on every layer, so `chain_points_with_postprocessing()`
orders them as a closed tour rather than an open chain. Up to ten points the
shortest tour is found exactly by the same dynamic programming with the first
point fixed; with a start point the tour then begins at the point nearest it.
Larger sets take the greedy chain and alternate 2-opt, or-opt and crossing
removal; or-opt moves runs of up to three points next to one of the eight points
nearest an end of the run, found on the same grid as the chain's candidates.
For longer chains, the search refines the greedy chain. On an island cut into many short pieces,
such as infill without anchors, that chain can sweep away from the nozzle and
come back for the rest with one long jump, which no local move below removes.
So a nearest-next chain, going from the nozzle to the nearest free path end each
time, is built too; when it is already shorter than the refined greedy chain, it
is refined and used instead. Elsewhere it costs one grid query per path.
`improve_chain_by_local_search()` sees the chain as a sequence of segments with
an entry and an exit point each. Its cost is the straight distance from the
nozzle to the first entry plus the distance from every exit to the next entry;
the end of the chain is free. It applies three kinds of moves:
- 2-opt reverses a run of segments, which flips each of them and replaces the
two links at the ends of the run.
- Or-opt moves a run of up to three segments between two others, flipped or
not.
- A run of any length that starts or ends next to the nozzle moves to the front.
Local search can settle on a chain that starts with a block of paths far from
the nozzle while the paths next to it sit in the middle, and moving that block
takes more than three segments at once.
A move that would flip a segment that cannot be reversed is skipped, so such
segments are only reordered.
A move is tried only if every link it creates joins an end point to one of the
32 end points nearest to it, which covers every move on a chain of up to 17
segments, as most islands are; with 8 candidates the search missed most of the
gain on grid-like patterns such as Triangles. The candidates come from a uniform
grid of about two end points per cell, searched in rings of cells around the
query. `KDTreeIndirect` visits its subtrees in a fixed order rather than the
nearer one first, so a query for 32 points walks most of the tree, and on long
chains finding the candidates would cost more than the search itself.
The candidates of an end point are tried nearest first, and only while they are
closer to it than the link the move would replace there. A 2-opt move that pays
off creates a link shorter than the one it replaces at one of its two ends, so
this misses no 2-opt move. An or-opt move is tried only while its new link is
shorter than what taking the run out saves, which skips a few or-opt moves that
would still pay off but cuts the search on long chains to a fraction.
The search works through a queue of segments and applies the first move that
shortens the chain around the segment it examines. After a move, only the
segments next to a changed link are queued again. A new link can also make room
for a run elsewhere that no segment next to it would try, so when the queue
empties after any move, every segment is queued once more, and the search ends
when such a sweep finds no move.
Every move shortens the chain by more than `SCALED_EPSILON`, so the result is
never longer than the greedy or the nearest-next chain and the search ends; a
cap of 50 moves per segment only bounds its time. The search has no randomness
and visits the segments in a fixed order, so the same input always gives the
same G-code.
## Cost
Finding the candidates of an end point visits a few cells, and the nearest-next
chain makes one such query per path, which only grows where the paths around the
nozzle are used up and the chain jumps. Examining a segment tries at most a few
hundred candidate links, usually only the nearest few, plus a scan of the chain
for the move to the front when it is the first segment, and a move rewrites only
the range of the chain it changes, so the search stays close to linear in the
number of segments. Most islands hold a few to a few dozen paths; the cost shows
on infill cut into thousands of pieces per island, such as dense infill with the
anchors off. The straight joins only look at the line segments near a candidate
join for crossings.
## Limitations
The search minimizes straight-line distance. It does not know which travels
leave the infill area and retract under `reduce_infill_retraction`, nor the
detours of `reduce_crossing_wall`, and a travel's time is not proportional to
its length: a short travel is bound by acceleration, so its time grows about
with the square root of its length. A chain that is shorter in total but trades
very short travels for longer ones, or for ones past `retraction_minimum_travel`,
can occasionally take a little longer to print.
The end of the chain is free: it is not steered toward the next island or the
start of the next layer. The regions of an island are chained one after the
other, in region order, each from where the previous one ended.
+3 -1
View File
@@ -36839,7 +36839,9 @@ msgstr ""
msgid ""
"Reverse on even\n"
"Did you know that <b>Reverse on even</b> feature can significantly improve the surface quality of your overhangs? However, it can cause wall inconsistencies so use carefully!"
msgstr "Реверс на чётных слояхСовет: настройка <b>Реверс на чётных слоях</b> может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью."
msgstr ""
"Реверс на чётных слоях\n"
"Совет: настройка <b>Реверс на чётных слоях</b> может существенно улучшить качество нависающих поверхностей. Как и нарушить их однородность, так что используйте с осторожностью."
#: resources/data/hints.ini: [hint:Cut Tool]
msgid ""
Binary file not shown.
Binary file not shown.
+126
View File
@@ -0,0 +1,126 @@
<#
.SYNOPSIS
Fails when a Windows build has an exception class with broken throw info.
.DESCRIPTION
Clang on the MSVC ABI can leave the copy constructor out of the CatchableType it emits for
an exception class that comes through a precompiled header
(https://github.com/llvm/llvm-project/issues/53486). The linker keeps one throw info per
class, so when it keeps such a record, a copy of that exception (std::exception_ptr, a catch
by value) shares its data with the original and the data is freed twice.
A class is either trivially copyable or not, so a binary should never have both a CatchableType
without a copy constructor (_CT??_R0?AV<class>@@@8<size>) and one with it
(_CT??_R0?AV<class>@@@8??0<class>...). The script lists each PDB's public symbols with the
llvm-pdbutil next to the build's compiler and reports every class that has both.
scripts\test_check_exception_throw_info.ps1 tests it.
.PARAMETER BuildDir
The slicer build directory. Its CMakeCache.txt names the compiler, and its src\<Config>
holds the PDBs to check, which must include OrcaSlicer.pdb.
.PARAMETER Config
The configuration whose PDBs to check (default: Release).
.PARAMETER Pdb
PDBs to check instead of the ones in <BuildDir>\src\<Config>, as a list or comma-separated.
.PARAMETER Dump
Saved `llvm-pdbutil dump --publics` output to check instead of PDBs. Needs no build.
.EXAMPLE
scripts\check_exception_throw_info.ps1 -BuildDir build
#>
param(
[string] $BuildDir,
[string] $Config = 'Release',
[string[]] $Pdb,
[string[]] $Dump
)
$ErrorActionPreference = 'Stop'
# powershell -File passes "a.pdb,b.pdb" as one string. A path that exists as given stays whole.
function Split-PathList([string[]] $values) {
@($values | Where-Object { $_ } | ForEach-Object {
if ((Test-Path -LiteralPath $_) -or $_ -notmatch ',') { $_ } else { $_ -split ',' | Where-Object { $_ } }
})
}
$Dump = Split-PathList $Dump
if (-not $Dump) {
if (-not $BuildDir) {
throw 'Name the slicer build directory with -BuildDir.'
}
$cache = Join-Path $BuildDir 'CMakeCache.txt'
if (-not (Test-Path $cache)) {
throw "No CMakeCache.txt in $BuildDir. Name the slicer build directory."
}
$entry = Select-String -Path $cache -Pattern '^CMAKE_CXX_COMPILER:\w+=(.+)$' | Select-Object -First 1
if (-not $entry) {
throw "No CMAKE_CXX_COMPILER in $cache."
}
$compiler = $entry.Matches[0].Groups[1].Value
# A tree configured with a bare compiler name found it on PATH.
if (-not (Split-Path $compiler)) {
$found = Get-Command $compiler -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $found) {
throw "$compiler, the compiler in $cache, is not on PATH."
}
$compiler = $found.Source
}
$pdbutil = Join-Path (Split-Path $compiler) 'llvm-pdbutil.exe'
if (-not (Test-Path $pdbutil)) {
throw "No llvm-pdbutil.exe next to $compiler. This check needs a build made with Clang."
}
$Pdb = Split-PathList $Pdb
if (-not $Pdb) {
# OrcaSlicer.dll holds the slicer itself, so a check without it proves nothing.
$pdbDir = Join-Path $BuildDir "src\$Config"
if (-not (Test-Path (Join-Path $pdbDir 'OrcaSlicer.pdb'))) {
throw "No OrcaSlicer.pdb in $pdbDir."
}
$Pdb = @(Get-ChildItem $pdbDir -Filter *.pdb | ForEach-Object FullName)
}
}
$catchable = [regex]'`_CT\?\?_R0\?A[UV](.+?)@@@8([^`]*)`'
$tmp = [IO.Path]::GetTempFileName()
$failed = $false
$classes = 0
try {
foreach ($file in $(if ($Dump) { $Dump } else { $Pdb })) {
$text = $file
if (-not $Dump) {
$run = Start-Process -FilePath $pdbutil -ArgumentList 'dump', '--publics', "`"$file`"" -NoNewWindow -Wait -PassThru -RedirectStandardOutput $tmp
if ($run.ExitCode -ne 0) {
throw "llvm-pdbutil failed on $file."
}
$text = $tmp
}
$kinds = @{}
foreach ($match in $catchable.Matches([IO.File]::ReadAllText((Resolve-Path -LiteralPath $text).Path))) {
$class = $match.Groups[1].Value
if (-not $kinds.ContainsKey($class)) { $kinds[$class] = @{} }
$kinds[$class][$(if ($match.Groups[2].Value.StartsWith('??0')) { 'copy' } else { 'bitwise' })] = $true
}
$broken = @($kinds.Keys | Where-Object { $kinds[$_].Count -eq 2 } | Sort-Object)
"$(Split-Path -Leaf $file): $($kinds.Count) exception classes, $($broken.Count) with broken throw info"
foreach ($class in $broken) { " $class" }
if ($broken.Count -gt 0) { $failed = $true }
$classes += $kinds.Count
}
} finally {
Remove-Item $tmp -ErrorAction SilentlyContinue
}
# The slicer's PDBs always contain exception throw info, so finding none means the dump format or the PDBs changed.
if ($classes -eq 0) {
'No exception classes found. Check that llvm-pdbutil still prints public symbols in backticks.'
$failed = $true
}
exit [int]$failed
+4
View File
@@ -301,6 +301,10 @@ $cases = @(
Contains = @('Precompiled header: off') }
@{ Name = 'the precompiled header is on unless asked'; Args = @('-s')
NotContains = @('SLIC3R_PCH') }
@{ Name = '-l turns the precompiled header off'; Args = @('-s', '-l')
Contains = @('-DSLIC3R_PCH=OFF', 'Precompiled header: off') }
@{ Name = 'a deps-only build says nothing about the precompiled header'; Args = @('-d', '-l')
NotContains = @('Precompiled header') }
@{ Name = '--no-pch works without a cache'; Args = @('-s', '--no-pch')
NotContains = @('COMPILER_LAUNCHER') }
@{ Name = 'the slicer build runs gettext'; Args = @('-s')
@@ -0,0 +1,93 @@
<#
.SYNOPSIS
Tests scripts/check_exception_throw_info.ps1 on canned llvm-pdbutil dumps.
.DESCRIPTION
Each case runs the check in its own powershell -File process, as CI does, and asserts on the
exit code and output. Dumps are written to a temporary folder, so no build is needed.
.EXAMPLE
powershell -File scripts/test_check_exception_throw_info.ps1
#>
$ErrorActionPreference = 'Stop'
$check = Join-Path $PSScriptRoot 'check_exception_throw_info.ps1'
$work = Join-Path ([IO.Path]::GetTempPath()) 'check_exception_throw_info_tests'
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $work | Out-Null
function Write-Dump([string] $path, [string[]] $symbols) {
New-Item -ItemType Directory -Force -Path (Split-Path $path) | Out-Null
$i = 0
$lines = @(' Public Symbols', '============================================================')
foreach ($symbol in $symbols) {
$i++
$lines += " $i | S_PUB32 [size = 48] ``$symbol``", ' flags = none, addr = 0007:0000'
}
Set-Content -Path $path -Encoding ascii -Value $lines
}
$bitwise = '_CT??_R0?AVfuture_error@std@@@840'
$copy = '_CT??_R0?AVfuture_error@std@@@8??0future_error@std@@QEAA@AEBV01@@Z40'
$trivial = '_CT??_R0?AUtag@@@81'
Write-Dump "$work\broken.txt" @($bitwise, $copy, $trivial)
Write-Dump "$work\clean.txt" @($copy, $trivial)
Write-Dump "$work\empty.txt" @()
Write-Dump "$work\a dir, with a comma\clean.txt" @($copy)
# A build directory whose compiler has an llvm-pdbutil beside it but no OrcaSlicer.pdb.
# Nothing runs the stand-in; the check stops before it gets there.
$build = "$work\build"
New-Item -ItemType Directory -Force -Path "$build\bin", "$build\src\Release" | Out-Null
Copy-Item "$env:SystemRoot\System32\where.exe" "$build\bin\llvm-pdbutil.exe"
Set-Content -Path "$build\CMakeCache.txt" -Encoding ascii -Value "CMAKE_CXX_COMPILER:FILEPATH=$build\bin\clang-cl.exe"
Set-Content -Path "$build\src\Release\orca-slicer.pdb" -Value ''
$bare = "$work\bare"
New-Item -ItemType Directory -Force -Path $bare | Out-Null
Set-Content -Path "$bare\CMakeCache.txt" -Encoding ascii -Value 'CMAKE_CXX_COMPILER:STRING=no-such-clang-cl.exe'
$cases = @(
@{ Name = 'a class with and without a copy constructor fails'; Args = @('-Dump', "$work\broken.txt")
Exit = 1; Contains = @('1 with broken throw info', 'future_error@std') }
@{ Name = 'copy constructors and trivially copyable classes pass'; Args = @('-Dump', "$work\clean.txt")
Exit = 0; Contains = @('2 exception classes, 0 with broken throw info') }
@{ Name = 'no exception classes at all fails'; Args = @('-Dump', "$work\empty.txt")
Exit = 1; Contains = @('No exception classes found') }
@{ Name = 'a comma-separated list checks each dump'; Args = @('-Dump', "$work\broken.txt,$work\clean.txt")
Exit = 1; Contains = @('broken.txt: 2 exception classes', 'clean.txt: 2 exception classes') }
@{ Name = 'a path with a comma stays whole'; Args = @('-Dump', "$work\a dir, with a comma\clean.txt")
Exit = 0; Contains = @('clean.txt: 1 exception classes') }
@{ Name = 'a build without OrcaSlicer.pdb fails'; Args = @('-BuildDir', $build)
Exit = 1; Contains = @('No OrcaSlicer.pdb in') }
@{ Name = 'a compiler that is not on PATH is named'; Args = @('-BuildDir', $bare)
Exit = 1; Contains = @('no-such-clang-cl.exe, the compiler in') }
@{ Name = 'a missing build directory is named'; Args = @('-BuildDir', "$work\nowhere")
Exit = 1; Contains = @('No CMakeCache.txt in') }
@{ Name = 'no build directory and no dump fails'; Args = @()
Exit = 1; Contains = @('Name the slicer build directory') }
)
$failed = 0
foreach ($case in $cases) {
$ErrorActionPreference = 'Continue'
$output = (& powershell -NoProfile -File $check @($case.Args) 2>&1 | Out-String)
$code = $LASTEXITCODE
$ErrorActionPreference = 'Stop'
$problems = @()
if ($code -ne $case.Exit) { $problems += "exit $code, expected $($case.Exit)" }
foreach ($text in $case.Contains) {
if (-not $output.Contains($text)) { $problems += "no '$text' in the output" }
}
if ($problems) {
$failed++
" FAIL $($case.Name)"
$problems | ForEach-Object { " $_" }
$output.TrimEnd() -split "`r?`n" | ForEach-Object { " | $_" }
} else {
" ok $($case.Name)"
}
}
Remove-Item -Recurse -Force $work -ErrorAction SilentlyContinue
"$($cases.Count - $failed) passed, $failed failed"
exit [int]($failed -gt 0)
+5 -2
View File
@@ -182,8 +182,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp
Fill/FillTpmsAdaptive.cpp
Fill/FillTpmsAdaptive.hpp
Fill/AdaptiveInfillField.cpp
Fill/AdaptiveInfillField.hpp
Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp
@@ -666,6 +666,9 @@ endif ()
encoding_check(libslic3r)
target_compile_definitions(libslic3r PUBLIC -DUSE_TBB -DTBB_USE_CAPTURED_EXCEPTION=0)
# CGAL changes Kd_tree's member layout when TBB is enabled. Match libslic3r_cgal
# in every consumer so inline constructors/destructors cannot mix the two layouts.
target_compile_definitions(libslic3r PUBLIC CGAL_LINKED_WITH_TBB)
if (USE_SLIC3R_CONSOLE_LOG)
target_compile_definitions(libslic3r PRIVATE $<$<CONFIG:RelWithDebInfo>:SLIC3R_CONSOLE_LOG>)
endif()
+1 -1
View File
@@ -616,7 +616,7 @@ struct PropagateDanielssonSingleStep {
if ((signs[addr] & 2) == 0) {
float *v = &L[addr << 1];
float l = v[0] * v[0] + v[1] * v[1];
float *v2s = v + (addr_delta << 1);
float *v2s = v + 2 * addr_delta;
float v2[2] = {
v2s[0] + INCX * resolution,
v2s[1] + INCY * resolution
@@ -1,4 +1,4 @@
#include "FillTpmsAdaptive.hpp"
#include "AdaptiveInfillField.hpp"
#include <algorithm>
#include <array>
@@ -146,11 +146,11 @@ void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, i
}
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
{
switch (gradient) {
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
case AdaptiveInfillGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case AdaptiveInfillGradient::Exponential: return std::pow(ratio, depth);
default: return 1. + (ratio - 1.) * depth;
}
}
@@ -159,7 +159,7 @@ double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
// continuous level of the target of a depth.
struct DensityLevels
{
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
DensityLevels(double ratio, double step, AdaptiveInfillGradient gradient)
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
{}
@@ -170,7 +170,7 @@ struct DensityLevels
}
double ratio;
TpmsAdaptiveGradient gradient;
AdaptiveInfillGradient gradient;
int count;
};
@@ -212,12 +212,12 @@ private:
} // namespace
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
const std::function<void()> &throw_if_canceled)
: m_mode(mode)
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
, m_axis(mode == AdaptiveInfillMode::NormalX ? 0 : mode == AdaptiveInfillMode::NormalY ? 1 : mode == AdaptiveInfillMode::NormalZ ? 2 : -1)
{
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
assert(!slices.empty() && mode != AdaptiveInfillMode::Disabled);
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
@@ -269,7 +269,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
// is the node nearest to the middle of the tied nodes.
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
const bool lobes = m_mode != AdaptiveInfillMode::SteppedShells && m_mode != AdaptiveInfillMode::SmoothBlend;
m_body.assign(depth.size(), -1);
std::vector<size_t> body_nodes;
for (size_t seed = 0; seed < depth.size(); ++seed) {
@@ -341,7 +341,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
}
throw_if_canceled();
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
if (m_mode == AdaptiveInfillMode::SteppedShells || m_mode == AdaptiveInfillMode::SmoothBlend || m_mode == AdaptiveInfillMode::DistanceWarp) {
m_depth.assign(depth.size(), 0.f);
for (size_t i = 0; i < depth.size(); ++i)
if (m_body[i] >= 0)
@@ -421,7 +421,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
lobe.reach.resize(log_reach.size());
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
if (m_mode == AdaptiveInfillMode::DistanceWarp) {
std::vector<double> mean(log_reach.size() * (Samples + 1));
for (size_t d = 0; d < log_reach.size(); ++d) {
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
@@ -488,7 +488,7 @@ TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const Boundin
}
}
double TpmsRadialField::depth(const Vec3d &pt) const
double AdaptiveInfillField::depth(const Vec3d &pt) const
{
assert(!m_depth.empty());
const Vec3d f = (pt - m_origin) / m_cell;
@@ -508,7 +508,7 @@ double TpmsRadialField::depth(const Vec3d &pt) const
return value;
}
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const
Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
{
Vec3d d = pt - center;
if (m_axis >= 0)
@@ -516,7 +516,7 @@ Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const
return d;
}
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
{
const Vec3d d = this->offset(pt, lobe.center);
const double r = d.norm();
@@ -562,7 +562,7 @@ double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
return warp(mean);
}
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
{
assert(!this->empty());
Vec3i32 idx;
@@ -588,7 +588,7 @@ size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
return count;
}
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
size_t AdaptiveInfillField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
{
const Body &body = m_bodies[m_body[node]];
if (body.lobes == 1) {
@@ -641,7 +641,7 @@ struct AdaptiveTpmsField
const long gsize = std::lround(gsizef / rsizef);
const AdaptiveTpms &tpms;
const TpmsRadialField &radial_field;
const AdaptiveInfillField &radial_field;
RadialScale scale;
DensityLevels levels;
Point size;
@@ -650,7 +650,7 @@ struct AdaptiveTpmsField
double cos_angle;
double sin_angle;
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
AdaptiveTpmsField(const AdaptiveTpms &tpms, const AdaptiveInfillField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
, size(bbox.size()), offs(bbox.min), z(z)
@@ -666,7 +666,7 @@ struct AdaptiveTpmsField
const double x = unscaled(pt.x());
const double y = unscaled(pt.y());
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
if (radial_field.mode() == AdaptiveInfillMode::SmoothBlend) {
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
auto lattice = [this, x, y](int level) {
const double frequency = tpms.surface_frequency * levels.scale(level);
@@ -681,7 +681,7 @@ struct AdaptiveTpmsField
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
}
const int axis = radial_field.axis();
TpmsRadialField::Radials radials;
AdaptiveInfillField::Radials radials;
const size_t count = radial_field.radial(obj, radials);
float value = 0.f;
for (size_t i = 0; i < count; ++i) {
@@ -708,20 +708,20 @@ template<> struct _RasterTraits<AdaptiveTpmsField>
};
// Continuous density level of the depth over a layer, in the object frame.
struct TpmsLevelField
struct AdaptiveLevelField
{
static constexpr float gsizef = 0.5f;
static constexpr float rsizef = 0.05f;
const coord_t rsize = scaled(rsizef);
const long gsize = std::lround(gsizef / rsizef);
const TpmsRadialField &field;
const AdaptiveInfillField &field;
const DensityLevels &levels;
Point size;
Point offs;
double z;
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
AdaptiveLevelField(const AdaptiveInfillField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
{}
@@ -736,19 +736,19 @@ struct TpmsLevelField
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
};
template<> struct _RasterTraits<TpmsLevelField>
template<> struct _RasterTraits<AdaptiveLevelField>
{
using ValueType = float;
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
static float get(const AdaptiveLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.x()); }
};
} // namespace marchsq
namespace Slic3r {
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
{
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
@@ -775,21 +775,21 @@ Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &fi
return polylines;
}
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, AdaptiveInfillGradient gradient)
{
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
if (levels.count == 0)
return {{surface_density, {expolygon}}};
// A fixed sampling grid, so that every region of a layer gets the same shells.
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
const coord_t cell = scaled(marchsq::AdaptiveLevelField::gsizef);
BoundingBox bbox = get_extents(expolygon);
bbox.offset(cell);
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
const marchsq::TpmsLevelField raster(field, levels, bbox, z);
const marchsq::AdaptiveLevelField raster(field, levels, bbox, z);
// Each level takes the part deeper than the middle between it and the previous one.
std::vector<TpmsShell> shells;
std::vector<AdaptiveInfillShell> shells;
ExPolygons remaining{expolygon};
for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
Polygons deeper;
@@ -808,12 +808,12 @@ std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPo
return shells;
}
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
{
FillParams shell_params = params;
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
shell_params.adaptive_infill_mode = AdaptiveInfillMode::Disabled;
for (const AdaptiveInfillShell &shell : make_adaptive_infill_shells(field, expolygon, z, params.density, params.adaptive_infill_interior_density, params.adaptive_infill_gradient)) {
shell_params.density = shell.density;
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
fill_shell(shell_params, part);
@@ -21,7 +21,7 @@ namespace Slic3r {
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
class TpmsRadialField
class AdaptiveInfillField
{
public:
struct Slice
@@ -39,7 +39,7 @@ public:
};
// Slices sorted by z, in the XY coordinates of the fill and the print Z.
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
const std::function<void()> &throw_if_canceled);
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
@@ -56,7 +56,7 @@ public:
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
int axis() const { return m_axis; }
TpmsAdaptiveMode mode() const { return m_mode; }
AdaptiveInfillMode mode() const { return m_mode; }
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
// the surface to 1.
@@ -91,7 +91,7 @@ private:
static constexpr int Polar = 24;
static constexpr int Azimuth = 48;
TpmsAdaptiveMode m_mode;
AdaptiveInfillMode m_mode;
int m_axis;
Vec3d m_origin;
double m_cell;
@@ -106,9 +106,9 @@ private:
std::vector<float> m_depth;
};
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
using AdaptiveInfillFieldPtr = std::unique_ptr<AdaptiveInfillField>;
// A field for every adaptive mode in use, indexed by the mode.
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
using AdaptiveInfillFields = std::array<AdaptiveInfillFieldPtr, size_t(AdaptiveInfillMode::Count)>;
struct AdaptiveTpms
{
@@ -117,14 +117,14 @@ struct AdaptiveTpms
// Pattern frequencies at the surface and at the center, in radians per mm.
double surface_frequency;
double interior_frequency;
TpmsAdaptiveGradient gradient;
AdaptiveInfillGradient gradient;
};
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
struct TpmsShell
struct AdaptiveInfillShell
{
float density;
ExPolygons expolygons;
@@ -132,12 +132,12 @@ struct TpmsShell
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
// middle of the layer.
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, AdaptiveInfillGradient gradient);
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
// so the lines connected along the boundaries of two shells don't overlap.
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r
+23 -23
View File
@@ -312,9 +312,9 @@ struct SurfaceFillParams
bool gyroid_optimized = false;
// For TPMS: grade the density from the surface to the interior of the object.
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
float tpms_interior_density = 0.f;
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
float adaptive_infill_interior_density = 0.f;
AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -358,9 +358,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -394,9 +394,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
this->tpms_adaptive == rhs.tpms_adaptive &&
this->tpms_interior_density == rhs.tpms_interior_density &&
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
@@ -1010,17 +1010,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
// (which would unnecessarily split fill batching). Adaptive density replaces it.
// Stored on SurfaceFillParams; copied to FillParams during conversion.
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1373,7 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1423,9 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// BBS
@@ -1598,7 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
@@ -1638,9 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds
+256 -13
View File
@@ -11,6 +11,10 @@
#include <string>
#include <vector>
#include <utility>
#include <unordered_map>
#include <unordered_set>
#include <tuple>
#include "../AABBTreeLines.hpp"
#include "../ClipperUtils.hpp"
#include "../EdgeGrid.hpp"
#include "../Geometry.hpp"
@@ -206,6 +210,9 @@ void Fill::fill_surface_extrusion(const Surface* surface, const FillParams& para
thick_polylines.clear();
}
else {
// Orca: sparse infill lines whose ends are next to each other print as one path, without a travel between them.
if (params.extrusion_role == erInternalInfill && ! eec->no_sort)
join_close_ends(polylines, scale_(2. * this->spacing), params.can_reverse);
extrusion_entities_append_paths(
eec->entities, std::move(polylines),
params.extrusion_role,
@@ -1644,6 +1651,8 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
};
const double line_half_width = 0.5 * scale_(spacing);
// Two ends whose anchors would meet are joined instead, with about as much wall.
const double join_length = anchor_length > SCALED_EPSILON ? std::max(anchor_length_max, 2. * (anchor_length + line_half_width)) : anchor_length_max;
#if 0
// Connection from end of one infill line to the start of another infill line.
@@ -1720,6 +1729,10 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
}
#endif
// Line ends not yet joined to another line along the contour.
std::vector<char> open_end(graph.map_infill_end_point_to_boundary.size(), 1);
auto end_index = [&graph](const ContourIntersectionPoint *cp) { return size_t(cp - graph.map_infill_end_point_to_boundary.data()); };
struct Arc {
ContourIntersectionPoint *intersection;
double arc_length;
@@ -1731,6 +1744,81 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (cp.contour_idx != boundary_idx_unconnected && cp.next_on_contour != &cp && cp.could_connect_next())
arches.push_back({ &cp, path_length_along_contour_ccw(&cp, cp.next_on_contour, graph.boundary_params[cp.contour_idx].back()) });
std::sort(arches.begin(), arches.end(), [](const auto& l, const auto& r) { return l.arc_length < r.arc_length; });
// Lines the arcs would join in this order.
auto count_joins = [&](const std::vector<Arc> &order) {
std::vector<char> consumed(graph.map_infill_end_point_to_boundary.size());
for (size_t i = 0; i < consumed.size(); ++ i)
consumed[i] = graph.map_infill_end_point_to_boundary[i].consumed;
std::vector<size_t> parent(infill_ordered.size());
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
size_t joins = 0;
for (const Arc &arc : order) {
const size_t end1 = end_index(arc.intersection), end2 = end_index(arc.intersection->next_on_contour);
if (consumed[end1] || consumed[end2] || (params.multiline > 1 && arc.arc_length < scale_(spacing) * params.multiline))
continue;
const size_t polyline1 = find(end1 / 2), polyline2 = find(end2 / 2);
if (polyline1 == polyline2)
continue;
if (arc.arc_length < join_length) {
parent[polyline2] = polyline1;
++ joins;
} else if (anchor_length <= SCALED_EPSILON)
continue;
consumed[end1] = consumed[end2] = 1;
}
// The last pass joins the open ends next to each other, also those of a multiline group.
for (size_t end = 0; end < consumed.size(); ++ end) {
const ContourIntersectionPoint &cp = graph.map_infill_end_point_to_boundary[end];
if (consumed[end] || cp.contour_idx == boundary_idx_unconnected)
continue;
const double length = graph.boundary_params[cp.contour_idx].back();
auto open = [&](const ContourIntersectionPoint *other, bool trimmed) {
return other != &cp && ! consumed[end_index(other)] && ! trimmed;
};
const double lprev = open(cp.prev_on_contour, cp.prev_trimmed || cp.prev_on_contour->next_trimmed) ?
path_length_along_contour_ccw(cp.prev_on_contour, &cp, length) : std::numeric_limits<double>::max();
const double lnext = open(cp.next_on_contour, cp.next_trimmed || cp.next_on_contour->prev_trimmed) ?
path_length_along_contour_ccw(&cp, cp.next_on_contour, length) : std::numeric_limits<double>::max();
consumed[end] = anchor_length > SCALED_EPSILON;
for (const double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l > join_length)
break;
const size_t other = end_index(l == lprev ? cp.prev_on_contour : cp.next_on_contour);
const size_t polyline1 = find(end / 2), polyline2 = find(other / 2);
if (polyline1 == polyline2)
continue;
parent[polyline2] = polyline1;
consumed[end] = consumed[other] = 1;
++ joins;
break;
}
}
return joins;
};
// Equal arcs taken along the contour pair a wall in one phase; keep the order that joins the most lines.
size_t best_joins = count_joins(arches);
const double bucket = 0.5 * scale_(spacing);
for (const bool backwards : { false, true }) {
std::vector<Arc> along_contour = arches;
std::sort(along_contour.begin(), along_contour.end(), [bucket, backwards](const Arc &l, const Arc &r) {
const long long bucket_l = std::llround(l.arc_length / bucket), bucket_r = std::llround(r.arc_length / bucket);
if (bucket_l != bucket_r)
return bucket_l < bucket_r;
if (l.intersection->contour_idx != r.intersection->contour_idx)
return l.intersection->contour_idx < r.intersection->contour_idx;
return backwards ? l.intersection->param > r.intersection->param : l.intersection->param < r.intersection->param;
});
if (const size_t joins = count_joins(along_contour); joins > best_joins) {
best_joins = joins;
arches = std::move(along_contour);
}
}
}
//FIXME improve the Traveling Salesman problem with 2-opt and 3-opt local optimization.
@@ -1752,7 +1840,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (polyline_idx1 != polyline_idx2) {
Polyline &polyline1 = infill_ordered[polyline_idx1];
Polyline &polyline2 = infill_ordered[polyline_idx2];
if (arc.arc_length < anchor_length_max) {
if (arc.arc_length < join_length) {
// Not closing a loop, connecting the lines.
assert(contour[cp1->point_idx] == polyline1.points.front() || contour[cp1->point_idx] == polyline1.points.back());
if (contour[cp1->point_idx] == polyline1.points.front())
@@ -1761,6 +1849,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse();
take(polyline1, polyline2, contour, cp1, cp2, false);
open_end[end_index(cp1)] = open_end[end_index(cp2)] = 0;
// Mark the second polygon as merged with the first one.
if (polyline_idx2 < polyline_idx1) {
polyline2 = std::move(polyline1);
@@ -1778,6 +1867,23 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
}
}
// Distance along the contour to the nearest open end of another polyline, within 64 ends.
auto distance_to_open_end = [&](const ContourIntersectionPoint &from, size_t polyline_idx, bool backwards) {
const double length = graph.boundary_params[from.contour_idx].back();
double distance = 0.;
const ContourIntersectionPoint *cp = &from;
for (size_t step = 0; step < 64; ++ step) {
const ContourIntersectionPoint *other = backwards ? cp->prev_on_contour : cp->next_on_contour;
if (other == nullptr || other == &from)
break;
distance += backwards ? closed_contour_distance_cw(cp->param, other->param, length) : closed_contour_distance_ccw(cp->param, other->param, length);
if (open_end[end_index(other)] && get_and_update_merged_with(end_index(other) / 2) != polyline_idx)
return distance;
cp = other;
}
return std::numeric_limits<double>::max();
};
// Connect the remaining open infill lines to the perimeter lines if possible.
for (ContourIntersectionPoint &contour_point : graph.map_infill_end_point_to_boundary)
if (! contour_point.consumed && contour_point.contour_idx != boundary_idx_unconnected) {
@@ -1796,7 +1902,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
assert(contour[contour_point.point_idx] == polyline.points.front() || contour[contour_point.point_idx] == polyline.points.back());
bool connected = false;
for (double l : { std::min(lprev, lnext), std::max(lprev, lnext) }) {
if (l == std::numeric_limits<double>::max() || l > anchor_length_max)
if (l == std::numeric_limits<double>::max() || l > join_length)
break;
// Take the complete contour.
bool reversed = l == lprev;
@@ -1815,6 +1921,7 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
if (contour[cp2->point_idx] == polyline2.points.back())
polyline2.reverse();
take(polyline, polyline2, contour, &contour_point, cp2, reversed);
open_end[end_index(&contour_point)] = open_end[end_index(cp2)] = 0;
if (polyline_idx < polyline_idx2) {
// Mark the second polyline as merged with the first one.
merged_with[polyline_idx2] = polyline_idx;
@@ -1829,18 +1936,35 @@ void Fill::connect_infill(Polylines &&infill_ordered, const std::vector<const Po
break;
}
if (! connected && anchor_length > SCALED_EPSILON) {
// Which to take? One could optimize for:
// 1) Shortest path
// 2) Hook length
// ...
// Let's take the longer now, as this improves the chance of another hook to be placed on the other side of this contour point.
double l = std::max(contour_point.contour_not_taken_length_prev, contour_point.contour_not_taken_length_next);
if (l > SCALED_EPSILON) {
if (contour_point.contour_not_taken_length_prev > contour_point.contour_not_taken_length_next)
take_limited(polyline, contour, contour_params, &contour_point, contour_point.prev_on_contour, true, anchor_length, line_half_width);
else
take_limited(polyline, contour, contour_params, &contour_point, contour_point.next_on_contour, false, anchor_length, line_half_width);
// Hook toward the nearest open end of another polyline, where the next path likely starts.
const double free_prev = contour_point.contour_not_taken_length_prev;
const double free_next = contour_point.contour_not_taken_length_next;
const double to_prev = distance_to_open_end(contour_point, polyline_idx, true);
const double to_next = distance_to_open_end(contour_point, polyline_idx, false);
bool backwards = free_prev > free_next;
if (to_prev < to_next && free_prev > SCALED_EPSILON)
backwards = true;
else if (to_next < to_prev && free_next > SCALED_EPSILON)
backwards = false;
// Orca: with multiline, a hook toward a copy stops short of it; the copy takes half a width over the sine of its wall angle.
double length = anchor_length;
if (const ContourIntersectionPoint *other = backwards ? contour_point.prev_on_contour : contour_point.next_on_contour;
params.multiline > 1 && other != &contour_point) {
const Point &end = contour[contour_point.point_idx];
Vec2d dir = Vec2d::Zero();
for (size_t i = 1; i < polyline.size() && dir.squaredNorm() < sqr(2. * scale_(spacing) * params.multiline); ++ i)
dir = (end - polyline.points[polyline.points.front() == end ? i : polyline.size() - 1 - i]).cast<double>();
const Vec2d to_copy = (contour[other->point_idx] - end).cast<double>();
const double across = dir.squaredNorm() > 0. ? std::abs(cross2(dir.normalized(), to_copy)) : 0.;
if (across > SCALED_EPSILON && across < scale_(spacing) * params.multiline) {
const double gap = backwards ? path_length_along_contour_ccw(other, &contour_point, contour_params.back()) :
path_length_along_contour_ccw(&contour_point, other, contour_params.back());
length = std::min(length, gap - line_half_width * (1. + to_copy.norm() / across));
}
}
if ((backwards ? free_prev : free_next) > SCALED_EPSILON)
take_limited(polyline, contour, contour_params, &contour_point, backwards ? contour_point.prev_on_contour : contour_point.next_on_contour,
backwards, std::max(0., length), line_half_width);
}
}
@@ -2798,4 +2922,123 @@ void multiline_fill(Polylines& polylines, const FillParams& params, float spacin
polylines = std::move(all_polylines);
}
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse)
{
const size_t n = polylines.size();
if (n < 2 || max_gap <= 0.)
return;
static constexpr size_t npos = size_t(-1);
// End 2 * i is the first point of polyline i, 2 * i + 1 its last.
auto end_point = [&polylines](size_t e) -> const Point & { return (e & 1) ? polylines[e >> 1].points.back() : polylines[e >> 1].points.front(); };
// Ends in cells of max_gap, so the ends within max_gap of one are in its cell or the eight around it.
auto cell_of = [max_gap](const Point &p) { return Vec2i64(int64_t(std::floor(p.x() / max_gap)), int64_t(std::floor(p.y() / max_gap))); };
auto key = [](const Vec2i64 &c) { return (uint64_t(c.x()) << 32) ^ uint64_t(uint32_t(c.y())); };
std::unordered_map<uint64_t, std::vector<size_t>> grid;
for (size_t e = 0; e < 2 * n; ++ e)
if (polylines[e >> 1].size() >= 2)
grid[key(cell_of(end_point(e)))].push_back(e);
// The end of a polyline that comes last along its overall direction, taken along the dominant axis.
std::vector<char> backwards(n, 0);
for (size_t i = 0; i < n; ++ i)
if (polylines[i].size() >= 2) {
const Vec2d dir = (polylines[i].points.back() - polylines[i].points.front()).cast<double>();
backwards[i] = std::abs(dir.x()) >= std::abs(dir.y()) ? dir.x() < 0. : dir.y() < 0.;
}
auto trailing = [&backwards](size_t e) { return bool(e & 1) != bool(backwards[e >> 1]); };
// Ends that meet first, then shortest gaps between leading ends, then the others: the copies of neighboring multiline
// groups are all joined at the same side, which leaves their open ends next to each other rather than on alternating walls.
std::vector<std::tuple<bool, bool, double, size_t, size_t>> gaps;
for (const auto &[cell_key, ends] : grid) {
const Vec2i64 c = cell_of(end_point(ends.front()));
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t a : ends)
for (size_t b : it->second)
if (a < b && (a >> 1) != (b >> 1) && (can_reverse || (a & 1) != (b & 1)))
if (const double d = (end_point(a) - end_point(b)).cast<double>().norm(); d <= max_gap)
gaps.emplace_back(d > SCALED_EPSILON, d > SCALED_EPSILON && (trailing(a) || trailing(b)), d, a, b);
}
if (gaps.empty())
return;
std::sort(gaps.begin(), gaps.end());
std::vector<size_t> partner(2 * n, npos), parent(n);
std::iota(parent.begin(), parent.end(), 0);
auto find = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
// A join may not cross a line or another join; touching them at its own ends is fine, as where a line ends on another.
// Only the segments through a cell next to a candidate join can cross one.
std::unordered_set<uint64_t> near_gaps;
for (const auto &[apart, trail, d, a, b] : gaps)
for (size_t e : { a, b })
for (int64_t dx = -1; dx <= 1; ++ dx)
for (int64_t dy = -1; dy <= 1; ++ dy)
near_gaps.insert(key(cell_of(end_point(e)) + Vec2i64(dx, dy)));
Lines near_lines;
for (const Polyline &pl : polylines)
for (size_t i = 1; i < pl.size(); ++ i) {
const Vec2i64 c0 = cell_of(pl.points[i - 1]), c1 = cell_of(pl.points[i]);
const Vec2i64 lo = c0.cwiseMin(c1), hi = c0.cwiseMax(c1);
bool near = (hi - lo).maxCoeff() > 3;
for (int64_t x = lo.x(); x <= hi.x() && ! near; ++ x)
for (int64_t y = lo.y(); y <= hi.y() && ! near; ++ y)
near = near_gaps.count(key(Vec2i64(x, y))) > 0;
if (near)
near_lines.emplace_back(pl.points[i - 1], pl.points[i]);
}
const AABBTreeLines::LinesDistancer<Line> lines(std::move(near_lines));
auto crosses = [&](size_t a, size_t b) {
const Vec2d p = end_point(a).cast<double>(), q = end_point(b).cast<double>();
const double length = (q - p).norm();
if (length <= 2. * SCALED_EPSILON)
return false;
const Vec2d dir = (q - p) / length;
const Line inner(Point((p + SCALED_EPSILON * dir).cast<coord_t>()), Point((q - SCALED_EPSILON * dir).cast<coord_t>()));
if (! lines.get_lines().empty() && ! lines.intersections_with_line<false>(inner).empty())
return true;
const Vec2i64 c = cell_of(end_point(a));
for (int64_t dx = -3; dx <= 3; ++ dx)
for (int64_t dy = -3; dy <= 3; ++ dy)
if (auto it = grid.find(key(c + Vec2i64(dx, dy))); it != grid.end())
for (size_t e : it->second)
if (partner[e] != npos && Geometry::segments_intersect(inner.a, inner.b, end_point(e), end_point(partner[e])))
return true;
return false;
};
for (const auto &[apart, trail, d, a, b] : gaps)
if (partner[a] == npos && partner[b] == npos && find(a >> 1) != find(b >> 1) && ! crosses(a, b)) {
partner[a] = b;
partner[b] = a;
parent[find(a >> 1)] = find(b >> 1);
}
// Walk each group from a polyline with a free end, entering the next polyline at the end joined to the last one.
Polylines out;
std::vector<char> done(n, 0);
auto walk = [&](size_t first_end) {
Polyline joined;
for (size_t e = first_end; e != npos;) {
Polyline &pl = polylines[e >> 1];
done[e >> 1] = 1;
if (e & 1)
pl.reverse();
joined.points.insert(joined.points.end(), pl.points.begin() + (! joined.empty() && joined.points.back() == pl.points.front()), pl.points.end());
e = partner[e ^ 1];
}
out.emplace_back(std::move(joined));
};
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i] == npos)
walk(2 * i);
for (size_t i = 0; i < n; ++ i)
if (! done[i] && partner[2 * i + 1] == npos)
walk(2 * i + 1);
polylines = std::move(out);
}
} // namespace Slic3r
+9 -6
View File
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r {
class Surface;
class TpmsRadialField;
class AdaptiveInfillField;
enum InfillPattern : int;
namespace FillAdaptive {
@@ -93,9 +93,9 @@ struct FillParams
bool gyroid_optimized { false };
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
float tpms_interior_density { 0.f };
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
float adaptive_infill_interior_density { 0.f };
AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -162,7 +162,7 @@ public:
FillAdaptive::Octree* adapt_fill_octree = nullptr;
// Radial coordinate inside the object for the adaptive TPMS infill
const TpmsRadialField* tpms_radial_field = nullptr;
const AdaptiveInfillField* adaptive_infill_field = nullptr;
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function.
@@ -258,8 +258,11 @@ public:
static coord_t _adjust_solid_spacing(const coord_t width, const coord_t distance);
};
//Fill Multiline
//Fill Multiline
void multiline_fill(Polylines& polylines, const FillParams& params, float spacing);
// Joins polylines whose ends are at most max_gap apart, shortest gaps first and without closing a loop, so each such
// group prints as one path. Without can_reverse only the end of one polyline joins the start of another.
void join_close_ends(Polylines &polylines, double max_gap, bool can_reverse);
} // namespace Slic3r
#endif // slic3r_FillBase_hpp_
+6 -6
View File
@@ -18,7 +18,7 @@
#include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "FillTpmsAdaptive.hpp"
#include "AdaptiveInfillField.hpp"
namespace Slic3r {
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
// generate pattern
Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
// Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
+6 -6
View File
@@ -17,7 +17,7 @@
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp"
#include "FillTpmsAdaptive.hpp"
#include "AdaptiveInfillField.hpp"
namespace Slic3r {
@@ -117,8 +117,8 @@ void FillTpmsD::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -130,13 +130,13 @@ void FillTpmsD::_fill_surface_single(
expolygon.rotate(-infill_angle);
Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
// Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
BoundingBox bbox = expolygon.contour.bounding_box();
bbox.offset(scale_((params.multiline + 1) * this->spacing));
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->adaptive_infill_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
} else {
BoundingBox bb = expolygon.contour.bounding_box();
// Density adjusted to have a good %of weight.
+7 -7
View File
@@ -8,7 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp"
#include "FillTpmsAdaptive.hpp"
#include "AdaptiveInfillField.hpp"
#include <cmath>
#include <algorithm>
#include <cstddef>
@@ -132,8 +132,8 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
ExPolygon expolygon,
Polylines& polylines_out)
{
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -151,10 +151,10 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing));
Polylines polylines;
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
params.tpms_adaptive_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.adaptive_infill_interior_density),
params.adaptive_infill_gradient},
*this->adaptive_infill_field, bbox, this->z, params.layer_height, spacing, infill_angle);
} else {
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
-8
View File
@@ -6660,10 +6660,6 @@ LayerResult GCode::process_layer(
case CalibMode::Calib_Input_shaping_freq: {
if (m_layer_index == 1){
gcode += writer().set_input_shaping('A', print.calib_params().start, 0.f, print.calib_params().shaper_type);
if (m_writer.get_gcode_flavor() == gcfKlipper) {
// Disable minimum cruise ratio to ensure consistent motion for calibration
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
}
} else {
if (print.calib_params().freqStartX == print.calib_params().freqStartY && print.calib_params().freqEndX == print.calib_params().freqEndY) {
gcode += writer().set_input_shaping('A', 0.f, this->interpolate_value_across_layers(print.calib_params().freqStartX, print.calib_params().freqEndX), "");
@@ -6676,10 +6672,6 @@ LayerResult GCode::process_layer(
}
case CalibMode::Calib_Input_shaping_damp: {
if (m_layer_index == 1){
if (m_writer.get_gcode_flavor() == gcfKlipper) {
// Disable minimum cruise ratio to ensure consistent motion for calibration
gcode += "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=0\n";
}
gcode += writer().set_input_shaping('X', 0.f, print.calib_params().freqStartX, print.calib_params().shaper_type);
gcode += writer().set_input_shaping('Y', 0.f, print.calib_params().freqStartY, print.calib_params().shaper_type);
} else {
+293 -100
View File
@@ -24,6 +24,7 @@
#include <algorithm>
#include <array>
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/algorithm/string/classification.hpp>
#include <boost/algorithm/string/constants.hpp>
#include <boost/log/trivial.hpp>
@@ -378,6 +379,11 @@ void GCodeProcessor::TimeMachine::reset()
travel_acceleration = 0.0f;
max_travel_acceleration = 0.0f;
extrude_factor_override_percentage = 1.0f;
klipper = false;
minimum_cruise_ratio = 0.5f;
requested_accel_to_decel = -1.0f;
klipper_junction_flush = 1.0f;
klipper_queue_priming = true;
time = 0.0f;
stop_times = std::vector<StopTime>();
curr.reset();
@@ -493,16 +499,87 @@ GCodeProcessor::TimeMachine::AdditionalBuffer GCodeProcessor::TimeMachine::merge
return merged;
}
// Klipper toolhead.py LookAheadQueue.flush: the pseudo-acceleration pass
// reserves cruising distance across a sequence, not separately in each segment.
size_t GCodeProcessor::TimeMachine::plan_klipper(bool lazy)
{
klipper_junction_flush = requested_accel_to_decel >= 0.0f ? 0.250f : 0.150f;
klipper_queue_priming = false;
size_t flush_count = blocks.size();
bool update_flush_count = lazy;
float next_start_v2 = 0.0f;
float next_mcr_start_v2 = 0.0f;
float peak_cruise_v2 = 0.0f;
size_t pending = 0;
for (size_t i = blocks.size(); i-- > 0;) {
TimeBlock& block = blocks[i];
const float delta_v2 = 2.0f * block.distance * block.acceleration;
const float reachable_start_v2 = next_start_v2 + delta_v2;
const float start_v2 = std::min(sqr(block.max_entry_speed), reachable_start_v2);
const float reachable_mcr_v2 = next_mcr_start_v2 + block.mcr_delta_v2;
const float mcr_start_v2 = std::min(block.max_mcr_entry_speed_sqr, reachable_mcr_v2);
float cruise_v2 = -1.0f;
++pending;
if (mcr_start_v2 < reachable_mcr_v2) {
if (mcr_start_v2 + block.mcr_delta_v2 > next_mcr_start_v2 || pending > 1) {
// Stop at this block, not past it: the blocks above still have a cruise speed
// that depends on moves not yet seen, so only [0, i) may be committed.
if (update_flush_count && peak_cruise_v2 > 0.0f) {
flush_count = i;
update_flush_count = false;
}
peak_cruise_v2 = std::min(sqr(block.feedrate_profile.cruise),
0.5f * (mcr_start_v2 + reachable_mcr_v2));
}
cruise_v2 = std::min({0.5f * (start_v2 + reachable_start_v2),
sqr(block.feedrate_profile.cruise), peak_cruise_v2});
pending = 0;
}
// Until the forward pass, these fields hold v^2, not speeds; -1 marks
// an unassigned cruise speed. A lazy return leaves them in this state.
block.feedrate_profile.entry = start_v2;
block.feedrate_profile.exit = next_start_v2;
block.trapezoid.cruise_feedrate = cruise_v2;
block.peak_cruise_v2 = peak_cruise_v2;
next_start_v2 = start_v2;
next_mcr_start_v2 = mcr_start_v2;
}
if (update_flush_count)
return 0;
// A run of blocks that cannot accelerate is resolved here, once the peak cruise speed of the
// accelerating block that precedes it is known. Each one takes a running minimum with its own
// entry speed, seeded from that peak rather than from the setter's own clamped cruise speed.
float prev_cruise_v2 = 0.0f;
for (size_t i = 0; i < flush_count; ++i) {
TimeBlock& block = blocks[i];
float cruise_v2 = block.trapezoid.cruise_feedrate;
if (cruise_v2 < 0.0f) {
cruise_v2 = std::min(prev_cruise_v2, block.feedrate_profile.entry);
prev_cruise_v2 = cruise_v2;
} else
prev_cruise_v2 = block.peak_cruise_v2;
const float start_v2 = std::min(block.feedrate_profile.entry, cruise_v2);
const float end_v2 = std::min(block.feedrate_profile.exit, cruise_v2);
// A zero acceleration would put an infinity in the ramp distances; the Marlin helpers guard
// their divides the same way and fall back to a pure cruise.
const float inv_two_accel = block.acceleration > 0.0f ? 0.5f / block.acceleration : 0.0f;
block.feedrate_profile.entry = std::sqrt(start_v2);
block.feedrate_profile.exit = std::sqrt(end_v2);
block.trapezoid.cruise_feedrate = std::sqrt(cruise_v2);
block.trapezoid.accelerate_until = (cruise_v2 - start_v2) * inv_two_accel;
block.trapezoid.decelerate_after = block.distance - (cruise_v2 - end_v2) * inv_two_accel;
}
return flush_count;
}
void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks, float additional_time, EMoveType target_move_type, bool is_final)
{
if (!enabled)
return;
// Orca: on the finalization pass, drain extra time that is still buffered (e.g. a
// trailing filament change) even with fewer than two blocks queued -- no later pass
// exists to attribute it on. Every other pass keeps the original >= 2 requirement,
// and an empty buffer keeps today's early-return unchanged (no behavior change).
// Drain buffered delays on the final pass, even with no motion left.
// A trailing filament change has no later move to receive its time.
const bool drain_final = is_final && !m_additional_time_buffer.empty();
if (blocks.size() < 2 && !drain_final) {
if ((klipper ? blocks.empty() : blocks.size() < 2) && !drain_final) {
// Not enough blocks to attribute the extra time to yet; buffer it so it is
// applied on a later pass instead of being dropped.
if (additional_time > 0.0f)
@@ -510,7 +587,7 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
return;
}
assert(keep_last_n_blocks <= blocks.size());
assert(klipper || keep_last_n_blocks <= blocks.size());
// Merge any previously buffered extra time with this call's extra time. Each
// entry is applied, in order, to the first block matching its target move type
@@ -520,23 +597,36 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
additional_buffer.emplace_back(target_move_type, additional_time);
additional_buffer = merge_adjacent_additional_time_blocks(additional_buffer);
// reverse_pass
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i) {
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
size_t n_blocks_process;
if (klipper) {
n_blocks_process = plan_klipper(keep_last_n_blocks != 0);
keep_last_n_blocks = blocks.size() - n_blocks_process;
} else {
for (int i = static_cast<int>(blocks.size()) - 1; i > 0; --i)
planner_reverse_pass_kernel(blocks[i - 1], blocks[i]);
for (size_t i = 0; i + 1 < blocks.size(); ++i)
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
recalculate_trapezoids(blocks);
n_blocks_process = blocks.size() - keep_last_n_blocks;
}
// forward_pass
for (size_t i = 0; i + 1 < blocks.size(); ++i) {
planner_forward_pass_kernel(blocks[i], blocks[i + 1]);
}
recalculate_trapezoids(blocks);
const size_t n_blocks_process = blocks.size() - keep_last_n_blocks;
size_t additional_buffer_idx = 0;
for (size_t i = 0; i < n_blocks_process; ++i) {
const TimeBlock& block = blocks[i];
float block_time = block.time();
double motion_time;
if (klipper && block.trapezoid.cruise_feedrate > 0.0f) {
// Integrate using the planned speeds. Reconstructing the exit speed
// from rounded deceleration distance loses precision near a stop.
const double cruise = block.trapezoid.cruise_feedrate;
motion_time = block.distance / cruise;
if (block.acceleration > 0.0f) {
const double accel_delta = cruise - block.feedrate_profile.entry;
const double decel_delta = cruise - block.feedrate_profile.exit;
motion_time += (sqr(accel_delta) + sqr(decel_delta)) / (2.0 * block.acceleration * cruise);
}
} else {
motion_time = block.time();
}
double block_time = motion_time;
if (additional_buffer_idx < additional_buffer.size()) {
const EMoveType buf_move_type = additional_buffer[additional_buffer_idx].first;
if (buf_move_type == EMoveType::Noop || buf_move_type == block.move_type) {
@@ -545,13 +635,13 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
}
}
time += double(block_time);
time += block_time;
// Orca: accumulate per-SkipType time spent inside SKIPPABLE regions, folded into this single
// calculate_time write site. Fires fleet-wide (the shipping time_lapse_gcode template stamps
// blocks stTimelapse), but writes only skippable_part_time, which has no g-code-emitting
// reader until the pre-heat injector consumes it, so output is byte-identical.
if (block.skippable_type != SkipType::stNone)
result.skippable_part_time[block.skippable_type] += block.time();
result.skippable_part_time[block.skippable_type] += motion_time;
result.moves[block.move_id].time[static_cast<size_t>(mode)] = block_time;
gcode_time.cache += block_time;
//BBS
@@ -689,14 +779,12 @@ void GCodeProcessor::TimeMachine::calculate_time(GCodeProcessorResult& result, P
if (keep_last_n_blocks) {
blocks.erase(blocks.begin(), blocks.begin() + n_blocks_process);
// Ensure that the new first block's entry speed will be preserved to prevent discontinuity
// between the erased blocks' exit speed and the new first block's entry speed.
// Otherwise, the first block's entry speed could be recalculated on the next pass without
// considering that there are no more blocks before this first block. This could lead
// to discontinuity between the exit speed (of already processed blocks) and the entry
// speed of the first block.
TimeBlock &first_block = blocks.front();
first_block.max_entry_speed = first_block.feedrate_profile.entry;
// Preserve the boundary speed in the Marlin planner. Klipper only flushes
// moves whose cruise speed is already independent of future moves.
if (!klipper) {
TimeBlock& first_block = blocks.front();
first_block.max_entry_speed = first_block.feedrate_profile.entry;
}
} else {
blocks.clear();
}
@@ -3872,9 +3960,9 @@ void GCodeProcessor::finalize(bool post_process)
update_slice_warnings();
}
float GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
double GCodeProcessor::get_time(PrintEstimatedStatistics::ETimeMode mode) const
{
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? float(m_time_processor.machines[static_cast<size_t>(mode)].time) : 0.0f;
return (mode < PrintEstimatedStatistics::ETimeMode::Count) ? m_time_processor.machines[static_cast<size_t>(mode)].time : 0.0;
}
float GCodeProcessor::get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const
@@ -5031,6 +5119,14 @@ void GCodeProcessor::process_G1(const GCodeReader::GCodeLine& line, const std::o
if (line.has_e()) g1_axes[E] = (double)line.e();
std::optional<double> g1_feedrate = std::nullopt;
if (line.has_f()) g1_feedrate = (double)line.f();
if (m_flavor == gcfKlipper) {
// Preserve decimal coordinates for short segments without parsing twice.
for (unsigned char axis = X; axis <= E; ++axis)
if (line.has(static_cast<Axis>(axis)))
g1_axes[axis] = line.precise_value(static_cast<Axis>(axis));
if (line.has_f())
g1_feedrate = line.precise_value(F);
}
process_G1(g1_axes, g1_feedrate);
}
@@ -5228,7 +5324,7 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
//BBS: limite the cruise according to centripetal acceleration
//Only need to handle when both prev and curr segment has movement in x-y plane
if ((prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
if (m_flavor != gcfKlipper && (prev.exit_direction(0) != 0.0f || prev.exit_direction(1) != 0.0f) &&
(curr.enter_direction(0) != 0.0f || curr.enter_direction(1) != 0.0f)) {
Vec3f v1 = prev.exit_direction;
v1(2, 0) = 0.0f;
@@ -5252,6 +5348,15 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
}
}
if (m_flavor == gcfKlipper && !is_extrusion_only_move(delta_pos)) {
const float max_velocity = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), X, m_machine_config_idx);
if (max_velocity > 0.0f)
curr.feedrate = std::min(curr.feedrate, max_velocity);
}
// Klipper applies E limits to retractions and moves without XY motion,
// not to positive extrusion along a printing path.
const bool limit_extruder = m_flavor != gcfKlipper || delta_pos[E] < 0.0 ||
(delta_pos[X] == 0.0 && delta_pos[Y] == 0.0);
// calculates block cruise feedrate
float min_feedrate_factor = 1.0f;
for (unsigned char a = X; a <= E; ++a) {
@@ -5260,7 +5365,8 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
curr.axis_feedrate[a] *= machine.extrude_factor_override_percentage;
curr.abs_axis_feedrate[a] = std::abs(curr.axis_feedrate[a]);
if (curr.abs_axis_feedrate[a] != 0.0f) {
if (curr.abs_axis_feedrate[a] != 0.0f && (a != E || limit_extruder) &&
(m_flavor != gcfKlipper || a == Z || a == E)) {
float axis_max_feedrate = get_axis_max_feedrate(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
if (axis_max_feedrate != 0.0f) min_feedrate_factor = std::min<float>(min_feedrate_factor, axis_max_feedrate / curr.abs_axis_feedrate[a]);
}
@@ -5277,20 +5383,42 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
}
// calculates block acceleration
float acceleration =
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(is_extrusion_only_move(delta_pos) ?
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
float acceleration;
if (m_flavor == gcfKlipper && is_extrusion_only_move(delta_pos)) {
// Klipper's max_extrude_only_accel. machine_max_acceleration_e is optional and some
// shipped profiles leave it at 0, so keep the non-zero retract fallback.
acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), E, m_machine_config_idx);
if (acceleration <= 0.0f)
acceleration = get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i));
} else
acceleration =
(m_flavor == gcfKlipper) ? get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(type == EMoveType::Travel) ? get_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
(is_extrusion_only_move(delta_pos) ?
get_retract_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)) :
get_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i)));
//BBS
for (unsigned char a = X; a <= E; ++a) {
if ((a == E && !limit_extruder) || (m_flavor == gcfKlipper && (a == X || a == Y)))
continue;
float axis_max_acceleration = get_axis_max_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), static_cast<Axis>(a), m_machine_config_idx);
// An unset (0) limit means "no limit", as in the feedrate loop above. Only the Klipper
// planner divides by the block acceleration, so leave the Marlin path as it was.
if (m_flavor == gcfKlipper && axis_max_acceleration <= 0.0f)
continue;
if (acceleration * std::abs(delta_pos[a]) * inv_distance > axis_max_acceleration)
acceleration = axis_max_acceleration / (std::abs(delta_pos[a]) * inv_distance);
}
block.acceleration = acceleration;
if (m_flavor == gcfKlipper) {
block.junction_deviation = get_junction_deviation(static_cast<PrintEstimatedStatistics::ETimeMode>(i), acceleration);
const float cruise_acceleration = machine.requested_accel_to_decel >= 0.0f ?
std::min(machine.acceleration, machine.requested_accel_to_decel) :
machine.acceleration * (1.0f - machine.minimum_cruise_ratio);
block.mcr_delta_v2 = 2.0f * distance * std::min(acceleration, cruise_acceleration);
}
static const float PREVIOUS_FEEDRATE_THRESHOLD = 0.0001f;
const bool has_prev_move = !blocks.empty() && prev.feedrate > PREVIOUS_FEEDRATE_THRESHOLD;
@@ -5401,6 +5529,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
block.feedrate_profile.entry = std::min(vmax_junction, v_allowable);
block.max_entry_speed = vmax_junction;
if (m_flavor == gcfKlipper && has_prev_move) {
const TimeBlock& previous = blocks.back();
block.max_entry_speed = std::min(block.max_entry_speed,
std::sqrt(sqr(previous.max_entry_speed) + 2.0f * previous.distance * previous.acceleration));
block.max_mcr_entry_speed_sqr = std::min(sqr(block.max_entry_speed), previous.max_mcr_entry_speed_sqr + previous.mcr_delta_v2);
}
block.flags.nominal_length = (block.feedrate_profile.cruise <= v_allowable);
block.flags.recalculate = true;
block.safe_feedrate = curr.safe_feedrate;
@@ -5412,9 +5546,12 @@ void GCodeProcessor::process_G1(const std::array<std::optional<double>, 4>& axes
prev = curr;
blocks.push_back(block);
if (m_flavor == gcfKlipper && blocks.size() > 1)
machine.klipper_junction_flush -= distance / curr.feedrate;
}
if (m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
if (m_flavor == gcfKlipper ? m_time_processor.machines[0].klipper_junction_flush <= 0.0f :
m_time_processor.machines[0].blocks.size() > TimeProcessor::Planner::refresh_threshold)
calculate_time(m_result, TimeProcessor::Planner::queue_size);
const Vec3f plate_offset = {(float) m_x_offset, (float) m_y_offset, 0.0f};
@@ -6341,6 +6478,8 @@ void GCodeProcessor::process_M108(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_M109(const GCodeReader::GCodeLine& line)
{
if (m_flavor == gcfKlipper)
simulate_st_synchronize();
int filament_id = get_filament_id();
float new_temp;
if (line.has_value('R', new_temp)) {
@@ -6403,6 +6542,8 @@ void GCodeProcessor::process_M140(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_M190(const GCodeReader::GCodeLine& line)
{
if (m_flavor == gcfKlipper)
simulate_st_synchronize();
float new_temp;
if (line.has_value('S', new_temp))
m_highest_bed_temp = m_highest_bed_temp < (int)new_temp ? (int)new_temp : m_highest_bed_temp;
@@ -6480,6 +6621,21 @@ void GCodeProcessor::process_M203(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_M204(const GCodeReader::GCodeLine& line)
{
float value;
if (m_flavor == gcfKlipper) {
if (!line.has_value('S', value)) {
float travel;
if (!line.has_value('P', value) || !line.has_value('T', travel))
return;
value = std::min(value, travel);
}
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
// setters would diverge from the firmware when custom G-code raises the limit.
if (value > 0.0f) {
for (auto& machine : m_time_processor.machines)
machine.acceleration = machine.travel_acceleration = value;
}
return;
}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal ||
m_time_processor.machine_envelope_processing_enabled) {
@@ -6541,50 +6697,50 @@ void GCodeProcessor::process_M205(const GCodeReader::GCodeLine& line)
void GCodeProcessor::process_SET_VELOCITY_LIMIT(const GCodeReader::GCodeLine& line)
{
// handle SQUARE_CORNER_VELOCITY
static const std::regex square_corner_velocity_pattern("\\sSQUARE_CORNER_VELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
std::smatch matches;
if (std::regex_search(line.raw(), matches, square_corner_velocity_pattern) && matches.size() == 2) {
float _jerk = 0;
try
{
_jerk = std::stof(matches[1]);
static const std::regex parameter_pattern(
R"(\s(VELOCITY|ACCEL|ACCEL_TO_DECEL|SQUARE_CORNER_VELOCITY|MINIMUM_CRUISE_RATIO)\s*=\s*([+]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?)(?=\s|;|$))",
std::regex::icase);
const std::string command = line.raw().substr(0, line.raw().find(';'));
std::vector<std::pair<std::string, float>> parameters;
for (std::sregex_iterator it(command.begin(), command.end(), parameter_pattern), end; it != end; ++it) {
float value;
try {
value = std::stof((*it)[2].str());
} catch (const std::exception&) {
continue;
}
catch (...){}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, _jerk);
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, _jerk);
if (std::isfinite(value))
parameters.emplace_back(boost::to_upper_copy((*it)[1].str()), value);
}
// Klipper 0.13 converts ACCEL_TO_DECEL only when the line has no MINIMUM_CRUISE_RATIO, and later
// releases ignore it, so a line carrying both (as Orca writes) is planned with the cruise ratio.
const bool has_cruise_ratio = std::any_of(parameters.begin(), parameters.end(),
[](const auto& parameter) { return parameter.first == "MINIMUM_CRUISE_RATIO" && parameter.second >= 0.0f && parameter.second < 1.0f; });
for (const auto& [name, value] : parameters) {
for (size_t i = 0; i < m_time_processor.machines.size(); ++i) {
TimeMachine& machine = m_time_processor.machines[i];
// Klipper replaces its single live max_accel for print and travel; the profile-clamping
// setters would diverge from the firmware when custom G-code raises the limit.
if (name == "ACCEL" && value > 0.0f)
machine.acceleration = machine.travel_acceleration = value;
else if (name == "MINIMUM_CRUISE_RATIO" && value >= 0.0f && value < 1.0f) {
if (machine.klipper_queue_priming && machine.requested_accel_to_decel >= 0.0f)
machine.klipper_junction_flush -= 1.0f;
machine.minimum_cruise_ratio = value;
machine.requested_accel_to_decel = -1.0f;
} else if (name == "ACCEL_TO_DECEL" && value > 0.0f && !has_cruise_ratio) {
if (machine.klipper_queue_priming && machine.requested_accel_to_decel < 0.0f)
machine.klipper_junction_flush += 1.0f;
machine.requested_accel_to_decel = value;
} else if (name == "SQUARE_CORNER_VELOCITY" && value >= 0.0f) {
set_option_value(m_time_processor.machine_limits.machine_max_jerk_x, i, value);
set_option_value(m_time_processor.machine_limits.machine_max_jerk_y, i, value);
} else if (name == "VELOCITY" && value > 0.0f) {
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, value);
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, value);
}
}
}
static const std::regex accel_pattern("\\sACCEL\\s*=\\s*([0-9]*\\.*[0-9]*)");
if (std::regex_search(line.raw(), matches, accel_pattern) && matches.size() == 2) {
float _accl = 0;
try
{
_accl = std::stof(matches[1]);
}
catch (...) {}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
set_travel_acceleration(static_cast<PrintEstimatedStatistics::ETimeMode>(i), _accl);
}
}
static const std::regex velocity_pattern("\\sVELOCITY\\s*=\\s*([0-9]*\\.*[0-9]*)");
if (std::regex_search(line.raw(), matches, velocity_pattern) && matches.size() == 2) {
float _speed = 0;
try
{
_speed = std::stof(matches[1]);
}
catch (...) {}
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
set_option_value(m_time_processor.machine_limits.machine_max_speed_x, i, _speed);
set_option_value(m_time_processor.machine_limits.machine_max_speed_y, i, _speed);
}
}
}
void GCodeProcessor::process_M221(const GCodeReader::GCodeLine& line)
@@ -6621,14 +6777,15 @@ void GCodeProcessor::process_M400(const GCodeReader::GCodeLine& line)
{
float value_s = 0.0;
float value_p = 0.0;
if (line.has_value('S', value_s) || line.has_value('P', value_p)) {
value_s += value_p * 0.001;
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
if (m_skip_end_gcode_delays)
return;
simulate_st_synchronize(value_s);
}
const bool has_delay = line.has_value('S', value_s) || line.has_value('P', value_p);
if (m_flavor != gcfKlipper && !has_delay)
return;
value_s += value_p * 0.001;
// Skip post-print end-gcode dwells so they don't inflate the M73 estimate (see
// m_skip_end_gcode_delays). Only omits dwell time — no state is updated here.
if (m_skip_end_gcode_delays && has_delay)
return;
simulate_st_synchronize(value_s);
}
void GCodeProcessor::process_M401(const GCodeReader::GCodeLine& line)
@@ -7460,12 +7617,12 @@ float GCodeProcessor::get_junction_deviation(PrintEstimatedStatistics::ETimeMode
{
const size_t id = static_cast<size_t>(mode);
// Klipper has no classic jerk: jd = scv^2 * (sqrt(2) - 1) / max_accel
// (toolhead.py::_calc_junction_deviation). Passing the block acceleration back in makes it cancel
// in calc_vmax_junction_deviation(), leaving the identity v == scv at a 90 degree corner.
// Klipper derives junction deviation from the live toolhead acceleration,
// before a move is constrained by Z or extruder limits.
if (m_flavor == gcfKlipper) {
// machine_max_jerk_x holds the square corner velocity; process_SET_VELOCITY_LIMIT() writes it.
const float scv = get_option_value(m_time_processor.machine_limits.machine_max_jerk_x, id);
acceleration = m_time_processor.machines[id].acceleration;
if (scv <= 0.0f || acceleration <= 0.0f)
return 0.0f;
return sqr(scv) * (std::sqrt(2.0f) - 1.0f) / acceleration;
@@ -7497,18 +7654,45 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
const TimeMachine::State& curr, bool has_prev_move,
PrintEstimatedStatistics::ETimeMode mode) const
{
if (m_flavor == gcfKlipper) {
if (!has_prev_move || curr.enter_direction.isZero() || prev.exit_direction.isZero())
return 0.0f;
const TimeBlock& previous = m_time_processor.machines[static_cast<size_t>(mode)].blocks.back();
double limit = std::min(sqr(double(block.feedrate_profile.cruise)), sqr(double(prev.feedrate)));
const double extrude_ratio_change = std::abs(curr.axis_feedrate[E] / curr.feedrate -
prev.axis_feedrate[E] / prev.feedrate);
// Klipper's instantaneous_corner_velocity defaults to 1 mm/s.
// It is independent of machine_max_jerk_e; custom firmware values
// cannot be inferred from the slicer's jerk setting.
if (extrude_ratio_change > 0.0f)
limit = std::min(limit, sqr(1.0 / extrude_ratio_change));
// Recover directions from the double-precision axis velocities: float
// unit vectors lose the angle between nearly collinear short segments.
const Vec3d prev_direction = Vec3d(prev.axis_feedrate[X], prev.axis_feedrate[Y], prev.axis_feedrate[Z]).normalized();
const Vec3d curr_direction = Vec3d(curr.axis_feedrate[X], curr.axis_feedrate[Y], curr.axis_feedrate[Z]).normalized();
const double cosine = std::clamp(-prev_direction.dot(curr_direction), -1.0, 1.0);
const double sin_half = std::sqrt(0.5 * (1.0 - cosine));
const double cos_half = std::sqrt(0.5 * (1.0 + cosine));
if (sin_half < 1.0 && cos_half > 0.0) {
const double radius = sin_half / (1.0 - sin_half);
const double half_tan = 0.5 * sin_half / cos_half;
limit = std::min({limit,
radius * block.junction_deviation * block.acceleration,
radius * previous.junction_deviation * previous.acceleration,
half_tan * block.distance * block.acceleration,
half_tan * previous.distance * previous.acceleration});
}
return std::sqrt(limit);
}
const float junction_deviation = get_junction_deviation(mode, block.acceleration);
if (junction_deviation <= 0.0f)
return -1.0f; // classic jerk machine, the caller keeps its own computation
if (!has_prev_move)
return 0.0f; // starts from rest, the planner raises this on the reverse pass
// -1 for a straight continuation, +1 for a full reversal. Half angle identity, no acos()/sin().
// Both vectors are unit length over XYZE, so this really is a cosine: scaling by 1 / distance
// instead, as PrusaSlicer does, leaves an E term that makes extruding corners look straighter
// than they are. Marlin normalizes over XYZE for any extruding move (planner.cpp, esteps > 0)
// and Klipper keeps E out of the cosine entirely (toolhead.py::Move.calc_junction); both agree
// that the corner is planned by its geometry, and normalizing matches them to within 1e-5.
// Unit vectors over XYZE keep this a cosine. Scaling by 1 / XYZ distance
// leaves an E term that makes extruding corners appear straighter.
// Marlin normalizes over XYZE; the Klipper branch above uses XYZ only.
float junction_cos_theta = (-prev.jd_unit_vec).dot(curr.jd_unit_vec);
if (junction_cos_theta > 0.999999f)
return 0.0f; // the path doubles back, the machine has to stop
@@ -7524,8 +7708,8 @@ float GCodeProcessor::calc_vmax_junction_deviation(const TimeBlock& block, const
float vmax_junction_sqr = (junction_acceleration * junction_deviation * sin_theta_d2) / (1.0f - sin_theta_d2);
// Marlin's JD_HANDLE_SMALL_SEGMENTS: a short move through a shallow corner is treated as an arc and
// capped by the centripetal acceleration it needs. Klipper has no equivalent.
if (m_flavor != gcfKlipper && block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
// capped by the centripetal acceleration it needs.
if (block.distance < 1.0f && junction_cos_theta < -0.7071067812f) {
// Fast acos(-t), max. error +-0.033rad. MinMax polynomial by W. Randolph Franklin:
// https://wrf.ecse.rpi.edu/Research/Short_Notes/arcsin/onlyelem.html
const float neg = junction_cos_theta < 0.0f ? -1.0f : 1.0f;
@@ -7700,6 +7884,7 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
std::vector<TimeMachine::ActualSpeedMove> actual_speed_moves;
for (size_t i = 0; i < static_cast<size_t>(PrintEstimatedStatistics::ETimeMode::Count); ++i) {
TimeMachine& machine = m_time_processor.machines[i];
machine.klipper = m_flavor == gcfKlipper;
machine.calculate_time(m_result, static_cast<PrintEstimatedStatistics::ETimeMode>(i), keep_last_n_blocks, additional_time, target_move_type, is_final);
if (static_cast<PrintEstimatedStatistics::ETimeMode>(i) == PrintEstimatedStatistics::ETimeMode::Normal)
actual_speed_moves = std::move(machine.actual_speed_moves);
@@ -7778,6 +7963,14 @@ void GCodeProcessor::calculate_time(GCodeProcessorResult& result, size_t keep_la
void GCodeProcessor::simulate_st_synchronize(float additional_time, EMoveType target_move_type)
{
calculate_time(m_result, 0, additional_time, target_move_type);
if (m_flavor == gcfKlipper) {
// ToolHead overrides the normal look-ahead window with BUFFER_TIME_HIGH
// at startup and after synchronization: 2 s on legacy, 1 s on modern Klipper.
for (TimeMachine& machine : m_time_processor.machines) {
machine.klipper_junction_flush = machine.requested_accel_to_decel >= 0.0f ? 2.0f : 1.0f;
machine.klipper_queue_priming = true;
}
}
}
void GCodeProcessor::update_estimated_times_stats()
@@ -7812,7 +8005,7 @@ double GCodeProcessor::extract_absolute_position_on_axis(Axis axis, const GCodeR
is_relative |= (m_e_local_positioning_type == EPositioningType::Relative);
const double lengthsScaleFactor = (m_units == EUnits::Inches) ? double(INCHES_TO_MM) : 1.0;
double ret = line.value(Slic3r::Axis(axis)) * lengthsScaleFactor;
double ret = line.precise_value(Slic3r::Axis(axis)) * lengthsScaleFactor;
// if (axis == E && m_use_volumetric_e)
// ret /= area_filament_cross_section;
return is_relative ? m_start_position[axis] + ret : m_origin[axis] + ret;
+16 -3
View File
@@ -91,12 +91,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
struct Mode
{
float time;
double time;
float prepare_time;
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> custom_gcode_times;
void reset() {
time = 0.0f;
time = 0.0;
prepare_time = 0.0f;
custom_gcode_times.clear();
custom_gcode_times.shrink_to_fit();
@@ -703,6 +703,12 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
float acceleration{ 0.0f }; // mm/s^2
float max_entry_speed{ 0.0f }; // mm/s
float safe_feedrate{ 0.0f }; // mm/s
float junction_deviation{ 0.0f };
float max_mcr_entry_speed_sqr{ 0.0f };
float mcr_delta_v2{ 0.0f };
// Peak cruise speed squared in effect when this block was planned. Only set on a block
// that can accelerate; the delayed blocks after it are seeded from its value.
float peak_cruise_v2{ 0.0f };
Flags flags;
FeedrateProfile feedrate_profile;
Trapezoid trapezoid;
@@ -782,6 +788,11 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
// hard limit for the travel acceleration, to which the firmware will clamp.
float max_travel_acceleration; // mm/s^2
float extrude_factor_override_percentage;
bool klipper{ false };
float minimum_cruise_ratio{ 0.5f };
float requested_accel_to_decel{ -1.0f };
float klipper_junction_flush{ 1.0f };
bool klipper_queue_priming{ true };
// We accumulate total print time in doubles to reduce the loss of precision
// while adding big floating numbers with small float numbers.
double time; // s
@@ -818,6 +829,8 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
// Merge adjacent buffer entries that target the same move type.
static AdditionalBuffer merge_adjacent_additional_time_blocks(const AdditionalBuffer& buffer);
size_t plan_klipper(bool lazy);
// additional_time is attributed to the first block matching target_move_type
// (EMoveType::Noop matches any block, i.e. the first processed block).
void calculate_time(GCodeProcessorResult& result, PrintEstimatedStatistics::ETimeMode mode, size_t keep_last_n_blocks = 0, float additional_time = 0.0f, EMoveType target_move_type = EMoveType::Noop, bool is_final = false);
@@ -1383,7 +1396,7 @@ inline constexpr float DEFAULT_FILAMENT_DENSITY = 1.245f;
void process_buffer(const std::string& buffer);
void finalize(bool post_process);
float get_time(PrintEstimatedStatistics::ETimeMode mode) const;
double get_time(PrintEstimatedStatistics::ETimeMode mode) const;
float get_prepare_time(PrintEstimatedStatistics::ETimeMode mode) const;
std::string get_time_dhm(PrintEstimatedStatistics::ETimeMode mode) const;
std::vector<std::pair<CustomGCode::Type, std::pair<float, float>>> get_custom_gcode_times(PrintEstimatedStatistics::ETimeMode mode, bool include_remaining) const;
@@ -8,6 +8,7 @@
#include "libslic3r/PrintConfig.hpp"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <cstdint>
@@ -153,6 +154,41 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
return improved;
}
std::vector<size_t> tsp_exact_cycle(const Points& centers)
{
const size_t n = centers.size();
std::vector<size_t> path(n);
std::iota(path.begin(), path.end(), 0);
if (n <= 3) return path;
// State mask * m + j: the points 1..m of mask visited after point 0, the last one j + 1.
const size_t m = n - 1;
assert(m <= 16);
std::vector<double> cost((size_t(1) << m) * m, std::numeric_limits<double>::max());
std::vector<size_t> from(cost.size(), size_t(-1));
auto dist = [&centers](size_t a, size_t b) { return (centers[a].cast<double>() - centers[b].cast<double>()).norm(); };
for (size_t j = 0; j < m; ++j)
cost[(size_t(1) << j) * m + j] = dist(0, j + 1);
for (size_t mask = 1; mask < (size_t(1) << m); ++mask)
for (size_t j = 0; j < m; ++j)
if (const double c = cost[mask * m + j]; c < std::numeric_limits<double>::max())
for (size_t k = 0; k < m; ++k)
if (!(mask >> k & 1)) {
const size_t next = (mask | size_t(1) << k) * m + k;
if (const double nc = c + dist(j + 1, k + 1); nc < cost[next]) {
cost[next] = nc;
from[next] = mask * m + j;
}
}
const size_t full = (size_t(1) << m) - 1;
size_t best = full * m;
for (size_t j = 1; j < m; ++j)
if (cost[full * m + j] + dist(j + 1, 0) < cost[best] + dist(best % m + 1, 0))
best = full * m + j;
for (size_t s = best, k = n; k-- > 1; s = from[s])
path[k] = s % m + 1;
return path;
}
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers)
{
size_t pn = path.size();
@@ -29,6 +29,9 @@ bool tsp_2opt_improve(std::vector<size_t>& path, const Points& centers, int max_
// Returns true if any crossing was removed.
bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers);
// The shortest cycle through the points, starting at point 0, by dynamic programming over subsets; for a few points only.
std::vector<size_t> tsp_exact_cycle(const Points& centers);
// Rotate the cycle so the closing edge (last -> first) is minimized.
void tsp_rotate_minimize_closing(std::vector<size_t>& path, const Points& centers);
+5 -2
View File
@@ -26,6 +26,7 @@
#include "ClipperUtils.hpp"
#include "libslic3r/Line.hpp"
#include "LocalesUtils.hpp"
#include "libslic3r/GCodeWriter.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/Point.hpp"
@@ -1528,8 +1529,10 @@ private:
gcode << "M204 P" << acceleration;
}
else if (m_gcode_flavor == gcfKlipper && m_accel_to_decel_enable) {
gcode << "SET_VELOCITY_LIMIT ACCEL_TO_DECEL=" << acceleration * m_accel_to_decel_factor / 100;
gcode << "\nM204 S" << acceleration;
// One line, so Klipper derives the limit from this acceleration rather than the previous one.
std::string limits = "SET_VELOCITY_LIMIT ACCEL=" + std::to_string(acceleration);
GCodeWriter::append_klipper_accel_to_decel(limits, acceleration, m_accel_to_decel_factor);
gcode << limits;
}
else {
// M204: Set default acceleration
+1 -1
View File
@@ -95,7 +95,7 @@ const char* GCodeReader::parse_line_internal(const char *ptr, const char *end, G
if (pend != c && is_end_of_word(*pend)) {
// The axis value has been parsed correctly.
if (axis != UNKNOWN_AXIS)
gline.m_axis[int(axis)] = float(v);
gline.m_axis[int(axis)] = v;
gline.m_mask |= 1 << int(axis);
c = pend;
} else
+11 -10
View File
@@ -35,7 +35,8 @@ public:
std::string_view axis_pos(char axis) const;
void clear() { m_raw.clear(); }
bool has(Axis axis) const { return (m_mask & (1 << int(axis))) != 0; }
float value(Axis axis) const { return m_axis[axis]; }
float value(Axis axis) const { return float(m_axis[axis]); }
double precise_value(Axis axis) const { return m_axis[axis]; }
bool has(char axis) const;
bool has_value(char axis, float &value) const;
// Parse value of an axis from raw string starting at axis_pos.
@@ -72,15 +73,15 @@ public:
bool has_p() const { return this->has(P); }
bool has_unknown_axis() const { return this->has(UNKNOWN_AXIS); }
float x() const { return m_axis[X]; }
float y() const { return m_axis[Y]; }
float z() const { return m_axis[Z]; }
float e() const { return m_axis[E]; }
float f() const { return m_axis[F]; }
float x() const { return float(m_axis[X]); }
float y() const { return float(m_axis[Y]); }
float z() const { return float(m_axis[Z]); }
float e() const { return float(m_axis[E]); }
float f() const { return float(m_axis[F]); }
// BBS: add I J P axis
float i() const { return m_axis[I]; }
float j() const { return m_axis[J]; }
float p() const { return m_axis[P]; }
float i() const { return float(m_axis[I]); }
float j() const { return float(m_axis[J]); }
float p() const { return float(m_axis[P]); }
static bool cmd_is(const std::string &gcode_line, const char *cmd_test) {
const char *cmd = GCodeReader::skip_whitespaces(gcode_line.c_str());
@@ -100,7 +101,7 @@ public:
}
private:
std::string m_raw;
float m_axis[NUM_AXES];
double m_axis[NUM_AXES];
uint32_t m_mask;
friend class GCodeReader;
};
+13 -8
View File
@@ -453,12 +453,11 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
gcode += separate_travel ? "M204 T" : "M204 P";
gcode += value;
} else if (FLAVOR_IS(gcfKlipper)) {
gcode.reserve(96);
gcode.reserve(128);
gcode += "SET_VELOCITY_LIMIT ACCEL=";
gcode += value;
if (this->config.accel_to_decel_enable) {
gcode += " ACCEL_TO_DECEL=";
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
if (GCodeWriter::full_gcode_comment)
gcode += " ; adjust ACCEL_TO_DECEL";
}
@@ -473,6 +472,14 @@ std::string GCodeWriter::set_acceleration_internal(Acceleration type, unsigned i
return gcode;
}
void GCodeWriter::append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor)
{
gcode += " ACCEL_TO_DECEL=";
gcode += float_to_string_decimal_point(acceleration * accel_to_decel_factor / 100);
gcode += " MINIMUM_CRUISE_RATIO=";
gcode += float_to_string_decimal_point(1. - accel_to_decel_factor / 100);
}
std::string GCodeWriter::set_jerk_xy(double jerk)
{
if (jerk < 0.01 || is_approx(jerk, m_last_jerk))
@@ -548,15 +555,13 @@ std::string GCodeWriter::set_accel_and_jerk(unsigned int acceleration, double je
return std::string();
std::string gcode;
gcode.reserve(96);
gcode.reserve(128);
gcode += "SET_VELOCITY_LIMIT";
if (set_acceleration) {
gcode += " ACCEL=";
gcode += std::to_string(acceleration);
if (this->config.accel_to_decel_enable) {
gcode += " ACCEL_TO_DECEL=";
gcode += float_to_string_decimal_point(acceleration * this->config.accel_to_decel_factor / 100);
}
if (this->config.accel_to_decel_enable)
append_klipper_accel_to_decel(gcode, acceleration, this->config.accel_to_decel_factor.value);
m_last_acceleration = acceleration;
}
if (set_jerk) {
+4
View File
@@ -71,6 +71,10 @@ public:
std::string set_jerk_xy(double jerk);
// Orca: set acceleration and jerk in one command for Klipper
std::string set_accel_and_jerk(unsigned int acceleration, double jerk);
// Orca: Klipper's accel_to_decel limit for `acceleration`, sent as the legacy ACCEL_TO_DECEL and as the
// equivalent MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL that replaced it. Klipper before 0.13 and
// vendor forks read only the first, current Klipper only the second, and 0.13 prefers the second.
static void append_klipper_accel_to_decel(std::string &gcode, unsigned int acceleration, double accel_to_decel_factor);
std::string set_junction_deviation(double junction_deviation);
std::string set_pressure_advance(double pa, int tool = -1) const;
std::string set_input_shaping(char axis, float damp, float freq, std::string type) const;
+2 -2
View File
@@ -443,8 +443,8 @@ coordf_t Layer::get_sparse_infill_max_void_area()
if (density == 0.)
return -1;
// Orca: the adaptive TPMS infill is as sparse as its interior density.
if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern))
density = std::min(density, std::max(1.f, float(config.tpms_interior_density)));
if (density < 100.f && config.adaptive_infill_mode != AdaptiveInfillMode::Disabled && is_adaptive_infill_pattern(pattern))
density = std::min(density, std::max(1.f, float(config.adaptive_infill_interior_density)));
//BBS: rough estimation and need to be optimized
double spacing = flow.scaled_spacing() * (100 - density) / density;
+3 -3
View File
@@ -1183,9 +1183,9 @@ static std::vector<std::string> s_Preset_print_options{
"is_infill_first",
"sparse_infill_density",
"fill_multiline",
"tpms_adaptive",
"tpms_interior_density",
"tpms_adaptive_gradient",
"adaptive_infill_mode",
"adaptive_infill_interior_density",
"adaptive_infill_gradient",
"gyroid_optimized",
"sparse_infill_pattern",
"sparse_infill_smooth_factor",
+4 -4
View File
@@ -12,7 +12,7 @@
#include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp"
#include "Fill/AdaptiveInfillField.hpp"
#include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp"
@@ -407,7 +407,7 @@ public:
double max_z() const { return m_max_z; }
// Centering offset of the sliced mesh from the scaled and rotated mesh of the model.
const Point& center_offset() const { return m_center_offset; }
const TpmsRadialField* tpms_radial_field(TpmsAdaptiveMode mode) const { return m_tpms_radial_fields[size_t(mode)].get(); }
const AdaptiveInfillField* adaptive_infill_field(AdaptiveInfillMode mode) const { return m_adaptive_infill_fields[size_t(mode)].get(); }
// BBS
void generate_support_preview();
@@ -655,7 +655,7 @@ private:
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data();
TpmsRadialFields prepare_tpms_radial_fields() const;
AdaptiveInfillFields prepare_adaptive_infill_fields() const;
// BBS
SupportNecessaryType is_support_necessary();
@@ -706,7 +706,7 @@ private:
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator;
TpmsRadialFields m_tpms_radial_fields;
AdaptiveInfillFields m_adaptive_infill_fields;
std::vector < VolumeSlices > firstLayerObjSliceByVolume;
std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups;
+111 -28
View File
@@ -366,24 +366,24 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder)
//Orca
static t_config_enum_values s_keys_map_TpmsAdaptiveMode{
{ "disabled", int(TpmsAdaptiveMode::Disabled) },
{ "distance_warp", int(TpmsAdaptiveMode::DistanceWarp) },
{ "smooth_blend", int(TpmsAdaptiveMode::SmoothBlend) },
{ "stepped_shells", int(TpmsAdaptiveMode::SteppedShells) },
{ "lobes", int(TpmsAdaptiveMode::Lobes) },
{ "normal_z", int(TpmsAdaptiveMode::NormalZ) },
{ "normal_y", int(TpmsAdaptiveMode::NormalY) },
{ "normal_x", int(TpmsAdaptiveMode::NormalX) },
static t_config_enum_values s_keys_map_AdaptiveInfillMode{
{ "disabled", int(AdaptiveInfillMode::Disabled) },
{ "distance_warp", int(AdaptiveInfillMode::DistanceWarp) },
{ "smooth_blend", int(AdaptiveInfillMode::SmoothBlend) },
{ "stepped_shells", int(AdaptiveInfillMode::SteppedShells) },
{ "lobes", int(AdaptiveInfillMode::Lobes) },
{ "normal_z", int(AdaptiveInfillMode::NormalZ) },
{ "normal_y", int(AdaptiveInfillMode::NormalY) },
{ "normal_x", int(AdaptiveInfillMode::NormalX) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode)
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillMode)
static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{
{ "linear", int(TpmsAdaptiveGradient::Linear) },
{ "quadratic", int(TpmsAdaptiveGradient::Quadratic) },
{ "exponential", int(TpmsAdaptiveGradient::Exponential) },
static t_config_enum_values s_keys_map_AdaptiveInfillGradient{
{ "linear", int(AdaptiveInfillGradient::Linear) },
{ "quadratic", int(AdaptiveInfillGradient::Quadratic) },
{ "exponential", int(AdaptiveInfillGradient::Exponential) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient)
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillGradient)
//BBS
static t_config_enum_values s_keys_map_PrintSequence {
@@ -3660,7 +3660,7 @@ void PrintConfigDef::init_fff_params()
def->max = 10; // Maximum number of lines for infill pattern
def->set_default_value(new ConfigOptionInt(1));
def = this->add("tpms_adaptive", coEnum);
def = this->add("adaptive_infill_mode", coEnum);
def->label = L("Adaptive density (experimental)");
def->category = L("Strength");
def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the "
@@ -3678,7 +3678,7 @@ void PrintConfigDef::init_fff_params()
"joined to the rest by a narrower neck, is graded towards its own center.\n"
" - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density "
"does not change along it.");
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveMode>::get_enum_values();
def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillMode>::get_enum_values();
def->enum_values.push_back("disabled");
def->enum_values.push_back("distance_warp");
def->enum_values.push_back("smooth_blend");
@@ -3696,9 +3696,9 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Normal Y"));
def->enum_labels.push_back(L("Normal X"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveMode>(TpmsAdaptiveMode::Disabled));
def->set_default_value(new ConfigOptionEnum<AdaptiveInfillMode>(AdaptiveInfillMode::Disabled));
def = this->add("tpms_interior_density", coPercent);
def = this->add("adaptive_infill_interior_density", coPercent);
def->label = L("Interior density");
def->category = L("Strength");
def->tooltip = L("Density of the adaptive infill at the center of the object.");
@@ -3708,7 +3708,7 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionPercent(5));
def = this->add("tpms_adaptive_gradient", coEnum);
def = this->add("adaptive_infill_gradient", coEnum);
def->label = L("Adaptive gradient");
def->category = L("Strength");
def->tooltip = L("How the density changes from the surface to the center of the object.\n"
@@ -3717,7 +3717,7 @@ void PrintConfigDef::init_fff_params()
"changes faster towards the center.\n"
"Exponential: the density changes quickly just below the surface and levels off towards "
"the center.");
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveGradient>::get_enum_values();
def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillGradient>::get_enum_values();
def->enum_values.push_back("linear");
def->enum_values.push_back("quadratic");
def->enum_values.push_back("exponential");
@@ -3725,7 +3725,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Quadratic"));
def->enum_labels.push_back(L("Exponential"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveGradient>(TpmsAdaptiveGradient::Linear));
def->set_default_value(new ConfigOptionEnum<AdaptiveInfillGradient>(AdaptiveInfillGradient::Linear));
// Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z
// (vertical) axis at low infill density to shorten the effective column length under
@@ -9665,6 +9665,12 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key = "change_filament_gcode";
} else if (opt_key == "bridge_fan_speed") {
opt_key = "overhang_fan_speed";
} else if (opt_key == "tpms_adaptive") {
opt_key = "adaptive_infill_mode";
} else if (opt_key == "tpms_interior_density") {
opt_key = "adaptive_infill_interior_density";
} else if (opt_key == "tpms_adaptive_gradient") {
opt_key = "adaptive_infill_gradient";
} else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") {
// ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the
// default value "1" meant "the first/active filament". The current scheme uses a dedicated
@@ -13487,12 +13493,30 @@ static std::map<t_custom_gcode_key, t_config_option_keys> s_CustomGcodeSpecificP
// Machine G-code
{"file_start_gcode", {}},
{"machine_start_gcode", {}},
{"machine_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id"}},
{"machine_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
"current_filament_id", "current_extruder_id", "current_nozzle_id",
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types"}},
{"before_layer_change_gcode", {"layer_num", "layer_z", "max_layer_z"}},
{"layer_change_gcode", {"layer_num", "layer_z", "max_layer_z"}},
{"timelapse_gcode", {"layer_num", "layer_z", "max_layer_z"}},
{"change_filament_gcode", {"layer_num", "layer_z", "max_layer_z", "next_extruder", "previous_extruder", "fan_speed",
{"layer_change_gcode", {"layer_num", "layer_z", "max_layer_z", "most_used_physical_extruder_id",
"current_filament_id", "current_nozzle_id", "curr_y_acceleration_limit",
"curr_accumulated_mass", "curr_layer_mass"}},
{"timelapse_gcode", {"layer_num", "layer_z", "max_layer_z", "most_used_physical_extruder_id",
"curr_physical_extruder_id", "timelapse_pos_x", "timelapse_pos_y",
"has_timelapse_safe_pos", "timelapse_inline_photo",
"farthest_point_timelapse_enabled", "clear_to_x0"}},
{"wrapping_detection_gcode", {"layer_num", "layer_z", "max_layer_z", "most_used_physical_extruder_id",
"curr_physical_extruder_id"}},
{"change_filament_gcode", {"layer_num", "layer_z", "max_layer_z", "next_extruder", "previous_extruder",
"current_hotend", "next_hotend", "current_nozzle_id", "next_nozzle_id",
"current_filament_id", "next_filament_id", "old_extruder_variant", "new_extruder_variant",
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types", "filament_retract_length_nc",
"new_extruder_retracted_length", "outer_wall_volumetric_speed", "fan_speed",
"first_flush_volume", "flush_length_1", "flush_length_2", "flush_length_3", "flush_length_4",
"flush_length", "flush_volumetric_speeds", "flush_temperatures", "filament_cooling_before_tower",
"wipe_avoid_perimeter", "wipe_avoid_pos_x", "is_prime_tower_interface",
"filament_tower_interface_purge_volume", "filament_tower_interface_print_temp",
"wipe_tower_center_pos_x", "wipe_tower_center_pos_y", "wipe_tower_center_pos_valid",
"temperature", "nozzle_temperature", "first_layer_temperature", "nozzle_temperature_initial_layer",
"new_filament_e_feedrate", "new_filament_temp", "new_retract_length",
"new_retract_length_toolchange", "old_filament_e_feedrate", "old_filament_temp", "old_retract_length",
"old_retract_length_toolchange", "relative_e_axis", "second_flush_volume", "toolchange_count", "toolchange_z",
@@ -13505,8 +13529,18 @@ static std::map<t_custom_gcode_key, t_config_option_keys> s_CustomGcodeSpecificP
{"machine_pause_gcode", {}},
{"template_custom_gcode", {}},
// Filament G-code
{"filament_start_gcode", {"filament_extruder_id"}},
{"filament_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id"}},
{"filament_start_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
"current_filament_id", "current_extruder_id", "current_nozzle_id",
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types",
"retraction_distance_when_cut", "long_retraction_when_cut",
"temperature", "nozzle_temperature", "first_layer_temperature",
"nozzle_temperature_initial_layer"}},
{"filament_end_gcode", {"layer_num", "layer_z", "max_layer_z", "filament_extruder_id",
"current_filament_id", "current_extruder_id", "current_nozzle_id",
"nozzle_diameter_at_nozzle_id", "nozzle_volume_types",
"retraction_distance_when_cut", "long_retraction_when_cut",
"temperature", "nozzle_temperature", "first_layer_temperature",
"nozzle_temperature_initial_layer"}},
};
const std::map<t_custom_gcode_key, t_config_option_keys>& custom_gcode_specific_placeholders()
@@ -13535,11 +13569,44 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
def->label = L("Filament extruder ID");
def->tooltip = L("The current extruder ID. The same as current_extruder.");
new_def("current_filament_id", coInt, L("Current filament ID"), L("Index of the currently active filament. The index is zero-based (first filament has index 0)."));
new_def("current_extruder_id", coInt, L("Current extruder ID"), L("Index of the currently active physical extruder. The index is zero-based (first extruder has index 0)."));
new_def("current_nozzle_id", coInt, L("Current nozzle ID"), L("Index of the currently active nozzle. The index is zero-based (first nozzle has index 0)."));
new_def("nozzle_diameter_at_nozzle_id", coFloats, L("Nozzle diameters"), L("Nozzle diameters indexed by nozzle ID."));
new_def("nozzle_volume_types", coStrings, L("Nozzle volume types"), L("Nozzle volume variants indexed by nozzle ID."));
new_def("retraction_distance_when_cut", coFloat, L("Retraction distance when cut"), L("Retraction distance used when cutting filament."));
new_def("long_retraction_when_cut", coBool, L("Long retraction when cut"), L("Whether long retraction is used when cutting filament."));
// layer_change_gcode
new_def("most_used_physical_extruder_id", coInt, L("Most-used physical extruder ID"), L("Index of the physical extruder used most often on the current layer. The index is zero-based (first extruder has index 0)."));
new_def("curr_y_acceleration_limit", coFloat, L("Current Y acceleration limit"), L("Current Y-axis acceleration limit after the bed-mass adjustment."));
new_def("curr_accumulated_mass", coFloat, L("Current accumulated mass"), L("Accumulated printed mass at the current layer."));
new_def("curr_layer_mass", coFloat, L("Current layer mass"), L("Printed mass of the previous layer."));
// timelapse_gcode
new_def("curr_physical_extruder_id", coInt, L("Current physical extruder ID"), L("Index of the physical extruder active for the current layer. The index is zero-based (first extruder has index 0)."));
new_def("timelapse_pos_x", coInt, L("Timelapse position X"), L("Selected X position for timelapse capture."));
new_def("timelapse_pos_y", coInt, L("Timelapse position Y"), L("Selected Y position for timelapse capture."));
new_def("has_timelapse_safe_pos", coBool, L("Has timelapse safe position"), L("Whether a safe timelapse position was found."));
new_def("timelapse_inline_photo", coBool, L("Timelapse inline photo"), L("Whether the timelapse photo is captured at the inline position."));
new_def("farthest_point_timelapse_enabled", coBool, L("Farthest-point timelapse enabled"), L("Whether farthest-point timelapse positioning is active."));
new_def("clear_to_x0", coBool, L("Clear to X0"), L("Whether the timelapse move is clear to X0."));
// change_filament_gcode
new_def("previous_extruder", coInt, L("Previous extruder"), L("Index of the extruder that is being unloaded. The index is zero based (first extruder has index 0)."));
new_def("next_extruder", coInt, L("Next extruder"), L("Index of the extruder that is being loaded. The index is zero based (first extruder has index 0)."));
new_def("current_hotend", coInt, L("Current hotend"), L("Zero-based extruder index of the outgoing filament. On Bambu printers it is -1, or the nozzle ID when a filament moves between nozzles across layers."));
new_def("next_hotend", coInt, L("Next hotend"), L("Zero-based extruder index of the incoming filament. On Bambu printers it is -1, or the nozzle ID when a filament moves between nozzles across layers."));
new_def("next_nozzle_id", coInt, L("Next nozzle ID"), L("Index of the nozzle associated with the incoming filament. The index is zero-based (first nozzle has index 0)."));
new_def("next_filament_id", coInt, L("Next filament ID"), L("Index of the incoming filament. The index is zero-based (first filament has index 0)."));
new_def("old_extruder_variant", coString, L("Old extruder variant"), L("Nozzle-volume variant of the outgoing extruder."));
new_def("new_extruder_variant", coString, L("New extruder variant"), L("Nozzle-volume variant of the incoming extruder."));
new_def("filament_retract_length_nc", coFloat, L("Filament nozzle-change retract length"), L("Nozzle-change retraction length of the outgoing filament."));
new_def("new_extruder_retracted_length", coFloat, L("New extruder retracted length"), L("Currently parked retraction length of the incoming extruder."));
new_def("outer_wall_volumetric_speed", coFloat, L("Outer wall volumetric speed"), L("Volumetric speed used for the outer wall."));
new_def("relative_e_axis", coBool, L("Relative e-axis"), L("Indicates if relative positioning is being used."));
new_def("toolchange_count", coInt, L("Toolchange count"), L("The number of toolchanges throughout the print."));
new_def("toolchange_z", coFloat, L("Toolchange Z"), L("The Z position during the toolchange."));
new_def("fan_speed", coNone, "", ""); //Option is no longer used and is zeroed by placeholder parser for compatability
new_def("old_retract_length", coFloat, L("Old retract length"), L("The retraction length of the previous filament."));
new_def("new_retract_length", coFloat, L("New retract length"), L("The retraction length of the new filament."));
@@ -13564,6 +13631,22 @@ CustomGcodeSpecificConfigDef::CustomGcodeSpecificConfigDef()
new_def("flush_length_2", coFloat, L("Flush Length 2"), L("The second flush length."));
new_def("flush_length_3", coFloat, L("Flush Length 3"), L("The third flush length."));
new_def("flush_length_4", coFloat, L("Flush Length 4"), L("The fourth flush length."));
new_def("flush_length", coFloat, L("Flush length"), L("Total flush length."));
new_def("flush_volumetric_speeds", coFloats, L("Flush volumetric speeds"), L("Flush volumetric speeds for each filament."));
new_def("flush_temperatures", coInts, L("Flush temperatures"), L("Flush temperatures for each filament."));
new_def("filament_cooling_before_tower", coFloats, L("Filament cooling before tower"), L("Temperature drop before entering filament tower"));
new_def("wipe_avoid_perimeter", coBool, L("Avoid wipe perimeter"), L("Whether travel should avoid the wipe perimeter."));
new_def("wipe_avoid_pos_x", coFloat, L("Wipe avoidance X"), L("X position used to avoid the wipe area."));
new_def("is_prime_tower_interface", coBool, L("Prime tower interface"), L("Whether the current purge is for a prime-tower interface."));
new_def("filament_tower_interface_purge_volume", coFloat, L("Interface layer purge length"), L("Purge length for prime tower interface layer (where different materials meet)."));
new_def("filament_tower_interface_print_temp", coInt, L("Filament tower interface print temperature"), L("Print temperature used for the filament tower interface."));
new_def("wipe_tower_center_pos_x", coFloat, L("Wipe tower center X"), L("X coordinate of the nozzle's wipe-tower wait position, 2 mm outside the tower edge."));
new_def("wipe_tower_center_pos_y", coFloat, L("Wipe tower center Y"), L("Y coordinate of the nozzle's wipe-tower wait position."));
new_def("wipe_tower_center_pos_valid", coBool, L("Wipe tower center position valid"), L("Whether the print has a wipe tower."));
new_def("temperature", coInts, L("Temperature"), L("Nozzle temperatures indexed by filament."));
new_def("nozzle_temperature", coInts, L("Nozzle temperature"), L("Nozzle temperatures indexed by filament."));
new_def("first_layer_temperature", coInts, L("First layer temperature"), L("First-layer nozzle temperatures indexed by filament."));
new_def("nozzle_temperature_initial_layer", coInts, L("Nozzle temperature initial layer"), L("First-layer nozzle temperatures indexed by filament."));
// change_extrusion_role_gcode
// The whole tooltip is one literal so that the GUI can look it up in the catalog.
+11 -11
View File
@@ -172,8 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
// Orca: Infill patterns laid out by an octree, which each connected body always gets of its own.
inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; }
// Orca: Infill patterns graded by the "tpms_adaptive" option.
inline bool is_tpms_adaptive_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
// Orca: Infill patterns graded by the "adaptive_infill_mode" option.
inline bool is_adaptive_infill_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
@@ -279,8 +279,8 @@ enum class SurfaceFillOrder {
Count,
};
// Orca: what the adaptive TPMS density follows: the 3D shape of the object, or its 2D sections normal to an axis.
enum class TpmsAdaptiveMode {
// Orca: what the adaptive infill density follows: the 3D shape of the object, or its 2D sections normal to an axis.
enum class AdaptiveInfillMode {
Disabled,
DistanceWarp,
SmoothBlend,
@@ -292,8 +292,8 @@ enum class TpmsAdaptiveMode {
Count,
};
// Orca: how the adaptive TPMS density changes from the object surface to its deepest point.
enum class TpmsAdaptiveGradient {
// Orca: how the adaptive infill density changes from the object surface to its deepest point.
enum class AdaptiveInfillGradient {
Linear,
Quadratic,
Exponential,
@@ -816,8 +816,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveGradient)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillGradient)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -1501,9 +1501,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline))
((ConfigOptionEnum<TpmsAdaptiveMode>, tpms_adaptive))
((ConfigOptionPercent, tpms_interior_density))
((ConfigOptionEnum<TpmsAdaptiveGradient>, tpms_adaptive_gradient))
((ConfigOptionEnum<AdaptiveInfillMode>, adaptive_infill_mode))
((ConfigOptionPercent, adaptive_infill_interior_density))
((ConfigOptionEnum<AdaptiveInfillGradient>, adaptive_infill_gradient))
((ConfigOptionBool, gyroid_optimized))
// Ironing options
((ConfigOptionEnum<IroningType>, ironing_type))
+13 -13
View File
@@ -33,7 +33,7 @@
#include "Fill/FillAdaptive.hpp"
#include "Fill/Fill.hpp"
#include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp"
#include "Fill/AdaptiveInfillField.hpp"
#include "format.hpp"
#include "AABBTreeIndirect.hpp"
#include "AABBTreeLines.hpp"
@@ -1252,19 +1252,19 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr();
}
TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
AdaptiveInfillFields PrintObject::prepare_adaptive_infill_fields() const
{
TpmsRadialFields fields;
std::array<bool, size_t(TpmsAdaptiveMode::Count)> modes{};
AdaptiveInfillFields fields;
std::array<bool, size_t(AdaptiveInfillMode::Count)> modes{};
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id)
if (const PrintRegionConfig &config = this->printing_region(region_id).config();
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_tpms_adaptive_pattern(config.sparse_infill_pattern))
modes[size_t(config.tpms_adaptive.value)] = true;
modes[size_t(TpmsAdaptiveMode::Disabled)] = false;
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_adaptive_infill_pattern(config.sparse_infill_pattern))
modes[size_t(config.adaptive_infill_mode.value)] = true;
modes[size_t(AdaptiveInfillMode::Disabled)] = false;
if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty())
return fields;
std::vector<TpmsRadialField::Slice> slices;
std::vector<AdaptiveInfillField::Slice> slices;
slices.reserve(m_layers.size());
BoundingBox bbox;
for (const Layer *layer : m_layers) {
@@ -1277,7 +1277,7 @@ TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
if (!modes[mode])
continue;
// Without a field, the infill falls back to the regular pattern.
auto field = std::make_unique<TpmsRadialField>(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); });
auto field = std::make_unique<AdaptiveInfillField>(slices, bbox, AdaptiveInfillMode(mode), [this]() { m_print->throw_if_canceled(); });
if (!field->empty())
fields[mode] = std::move(field);
}
@@ -1685,9 +1685,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_overhang_angle") {
steps.emplace_back(posInfill);
} else if (opt_key == "sparse_infill_pattern"
|| opt_key == "tpms_adaptive"
|| opt_key == "tpms_interior_density"
|| opt_key == "tpms_adaptive_gradient"
|| opt_key == "adaptive_infill_mode"
|| opt_key == "adaptive_infill_interior_density"
|| opt_key == "adaptive_infill_gradient"
// Orca: Body centering now also determines bridge anchors during preparation.
// Invalidating preparation also invalidates infill, including top/bottom surfaces.
|| opt_key == "center_of_surface_pattern"
@@ -3195,7 +3195,7 @@ void PrintObject::bridge_over_infill()
}
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
this->m_tpms_radial_fields = this->prepare_tpms_radial_fields();
this->m_adaptive_infill_fields = this->prepare_adaptive_infill_fields();
std::vector<size_t> layers_to_generate_infill;
for (const auto &pair : surfaces_by_layer) {
+484 -11
View File
@@ -26,8 +26,10 @@
#include "Print.hpp"
#include "GCode/OrderingStrategies.hpp"
#include <array>
#include <cmath>
#include <cassert>
#include <deque>
#include "MultiMaterialSegmentation.hpp"
namespace Slic3r {
@@ -96,6 +98,46 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
return out;
}
// The shortest chain over every order and allowed reversal, by dynamic programming over the subsets of segments, so only
// for a few of them. end_points holds the first and the last point of each segment; the chain starts at start when given
// and its end is free. Return index and "reversed" flag.
static std::vector<std::pair<size_t, bool>> chain_segments_exact(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d *start)
{
const size_t n = could_reverse.size();
assert(n >= 1 && n <= 12 && end_points.size() == 2 * n);
// State (mask * n + j) * 2 + o: the segments of mask visited, the last one j, flipped when o.
const size_t num_states = (size_t(1) << n) * n * 2;
std::vector<double> cost(num_states, std::numeric_limits<double>::max());
std::vector<size_t> from(num_states, size_t(-1));
auto entry = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + o]; };
auto exit = [&end_points](size_t j, size_t o) -> const Vec2d & { return end_points[2 * j + 1 - o]; };
for (size_t j = 0; j < n; ++ j)
for (size_t o = 0; o <= size_t(could_reverse[j] != 0); ++ o)
cost[((size_t(1) << j) * n + j) * 2 + o] = start == nullptr ? 0. : (entry(j, o) - *start).norm();
for (size_t mask = 1; mask < (size_t(1) << n); ++ mask)
for (size_t s = mask * n * 2; s < (mask + 1) * n * 2; ++ s)
if (cost[s] < std::numeric_limits<double>::max())
for (size_t k = 0; k < n; ++ k)
if (! (mask >> k & 1))
for (size_t o = 0; o <= size_t(could_reverse[k] != 0); ++ o) {
const size_t next = (((mask | size_t(1) << k) * n) + k) * 2 + o;
const double c = cost[s] + (entry(k, o) - exit(s / 2 % n, s & 1)).norm();
if (c < cost[next]) {
cost[next] = c;
from[next] = s;
}
}
const size_t full = (size_t(1) << n) - 1;
size_t best = full * n * 2;
for (size_t s = best; s < num_states; ++ s)
if (cost[s] < cost[best])
best = s;
std::vector<std::pair<size_t, bool>> out(n);
for (size_t s = best, k = n; k -- > 0; s = from[s])
out[k] = { s / 2 % n, (s & 1) != 0 };
return out;
}
// Chain perimeters (always closed) and thin fills (closed or open) using a greedy algorithm.
// Solving a Traveling Salesman Problem (TSP) with the modification, that the sites are not always points, but points and segments.
// Solving using a greedy algorithm, where a shortest edge is added to the solution if it does not produce a bifurcation or a cycle.
@@ -104,6 +146,7 @@ std::vector<std::pair<size_t, bool>> chain_segments_closest_point(std::vector<En
// The algorithm builds a tour for the traveling salesman one edge at a time and thus maintains multiple tour fragments, each of which
// is a simple path in the complete graph of cities. At each stage, the algorithm selects the edge of minimal cost that either creates
// a new fragment, extends one of the existing paths or creates a cycle of length equal to the number of cities.
// Orca: two to four segments are chained exactly instead.
template<typename PointType, typename SegmentEndPointFunc, bool REVERSE_COULD_FAIL, typename CouldReverseFunc>
std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals_(SegmentEndPointFunc end_point_func, CouldReverseFunc could_reverse_func, size_t num_segments, const PointType *start_near)
{
@@ -118,6 +161,19 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy_constrained_reversals
out.emplace_back(0, could_reverse_func(0) && start_near != nullptr &&
(end_point_func(0, false) - *start_near).template cast<double>().squaredNorm() < (end_point_func(0, true) - *start_near).template cast<double>().squaredNorm());
}
else if (num_segments <= 4)
{
// Orca: so few segments are cheap to chain exactly. The first one is then not always the one nearest start_near.
std::vector<Vec2d> end_points(2 * num_segments);
std::vector<char> could_reverse(num_segments);
for (size_t i = 0; i < num_segments; ++ i) {
end_points[2 * i] = end_point_func(i, true).template cast<double>();
end_points[2 * i + 1] = end_point_func(i, false).template cast<double>();
could_reverse[i] = could_reverse_func(i);
}
const Vec2d start = start_near != nullptr ? Vec2d(start_near->template cast<double>()) : Vec2d::Zero();
out = chain_segments_exact(end_points, could_reverse, start_near != nullptr ? &start : nullptr);
}
else
{
// End points of segments for the KD tree closest point search.
@@ -1039,12 +1095,370 @@ std::vector<std::pair<size_t, bool>> chain_segments_greedy2(SegmentEndPointFunc
return chain_segments_greedy_constrained_reversals2_<PointType, SegmentEndPointFunc, false, decltype(could_reverse_func)>(end_point_func, could_reverse_func, num_segments, start_near);
}
// The K points nearest to a query, nearest first, ties by index, from a uniform grid of about two points per cell.
template<size_t K>
class NearestPoints
{
public:
static constexpr size_t npos = size_t(-1);
explicit NearestPoints(const std::vector<Vec2d> &points) : m_points(points)
{
BoundingBoxf bbox;
for (const Vec2d &p : points)
bbox.merge(p);
const Vec2d size = bbox.size();
const double count = double(std::max<size_t>(points.size(), 1));
m_cell = std::max(std::sqrt(2. * size.x() * size.y() / count), std::max(size.x(), size.y()) / count);
if (m_cell <= 0.)
m_cell = 1.;
m_origin = bbox.min;
m_cols = int(size.x() / m_cell) + 1;
m_rows = int(size.y() / m_cell) + 1;
m_first.assign(size_t(m_cols) * m_rows + 1, 0);
for (const Vec2d &p : points)
++ m_first[cell(p) + 1];
for (size_t c = 1; c < m_first.size(); ++ c)
m_first[c] += m_first[c - 1];
m_index.resize(points.size());
std::vector<size_t> next(m_first.begin(), m_first.end() - 1);
for (size_t i = 0; i < points.size(); ++ i)
m_index[next[cell(points[i])] ++] = i;
}
// Indices of the nearest points that filter accepts, npos past the last one.
template<typename Filter>
std::array<size_t, K> query(const Vec2d &q, Filter filter) const
{
std::array<std::pair<double, size_t>, K> best;
size_t found = 0;
const int cx = int(std::clamp(std::floor((q.x() - m_origin.x()) / m_cell), 0., double(m_cols - 1)));
const int cy = int(std::clamp(std::floor((q.y() - m_origin.y()) / m_cell), 0., double(m_rows - 1)));
auto visit = [&](int x, int y) {
if (x < 0 || y < 0 || x >= m_cols || y >= m_rows)
return;
const size_t c = size_t(y) * m_cols + x;
for (size_t k = m_first[c]; k < m_first[c + 1]; ++ k) {
const size_t i = m_index[k];
if (! filter(i))
continue;
const std::pair<double, size_t> candidate((m_points[i] - q).squaredNorm(), i);
if (found == K && ! (candidate < best[K - 1]))
continue;
auto it = std::upper_bound(best.begin(), best.begin() + found, candidate);
std::move_backward(it, best.begin() + std::min(found, K - 1), best.begin() + std::min(found + 1, K));
*it = candidate;
found = std::min(found + 1, K);
}
};
// Points beyond ring r are at least r cells away.
for (int r = 0; r <= std::max(m_cols, m_rows); ++ r) {
if (r == 0)
visit(cx, cy);
for (int d = -r; d <= r && r > 0; ++ d) {
visit(cx + d, cy - r);
visit(cx + d, cy + r);
if (d != -r && d != r) {
visit(cx - r, cy + d);
visit(cx + r, cy + d);
}
}
if (found == K && best[K - 1].first <= sqr(r * m_cell))
break;
}
std::array<size_t, K> out;
out.fill(npos);
for (size_t k = 0; k < found; ++ k)
out[k] = best[k].second;
return out;
}
private:
size_t cell(const Vec2d &p) const
{
const int x = std::clamp(int((p.x() - m_origin.x()) / m_cell), 0, m_cols - 1);
const int y = std::clamp(int((p.y() - m_origin.y()) / m_cell), 0, m_rows - 1);
return size_t(y) * m_cols + x;
}
const std::vector<Vec2d> &m_points;
Vec2d m_origin;
double m_cell;
int m_cols;
int m_rows;
std::vector<size_t> m_first;
std::vector<size_t> m_index;
};
// Shortens the travel of a chain with 2-opt moves (reversing a run of segments) and or-opt moves (moving a run of up to
// three segments elsewhere, or a longer one to the front, flipped or not). end_points holds the first and the last point
// of each segment. The chain starts at start_near when given, and its end is free. A move that would flip a segment that
// cannot reverse is skipped. Moves only create links between nearest neighbor end points, and after a move only the
// segments next to a changed link are examined again, until a sweep of all segments finds no move. This keeps the search
// close to linear.
static void improve_chain_by_local_search(std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points,
const std::vector<char> &could_reverse, const Vec2d *start_near)
{
const size_t n = chain.size();
if (n < 2)
return;
assert(end_points.size() == 2 * n && could_reverse.size() == n);
std::vector<size_t> order(n), pos(n), fixed_before(n + 1, 0);
std::vector<char> flipped(n);
for (size_t k = 0; k < n; ++ k) {
order[k] = chain[k].first;
flipped[order[k]] = chain[k].second;
}
// Updates positions lo..hi after a move, which keeps the number of segments that cannot reverse past hi.
auto refresh = [&](size_t lo, size_t hi) {
for (size_t k = lo; k <= hi; ++ k) {
pos[order[k]] = k;
fixed_before[k + 1] = fixed_before[k] + ! could_reverse[order[k]];
}
};
refresh(0, n - 1);
auto entry_idx = [&](size_t k) { return 2 * order[k] + flipped[order[k]]; };
auto exit_idx = [&](size_t k) { return 2 * order[k] + 1 - flipped[order[k]]; };
auto entry = [&](size_t k) -> const Vec2d& { return end_points[entry_idx(k)]; };
auto exit = [&](size_t k) -> const Vec2d& { return end_points[exit_idx(k)]; };
auto is_exit = [&](size_t e) { return char(e & 1) != flipped[e >> 1]; };
auto reversible = [&](size_t i, size_t j) { return fixed_before[j + 1] == fixed_before[i]; };
auto dist = [](const Vec2d *a, const Vec2d *b) { return a == nullptr || b == nullptr ? 0. : (*a - *b).norm(); };
// Travel end points around position k, null where the chain is open.
auto prev_exit = [&](size_t k) -> const Vec2d* { return k > 0 ? &exit(k - 1) : start_near; };
auto next_entry = [&](size_t k) -> const Vec2d* { return k + 1 < n ? &entry(k + 1) : nullptr; };
// The 32 nearest end points are the candidates, which covers every move on short chains; 8 missed much of the gain.
static constexpr size_t K = 32;
static constexpr size_t npos = NearestPoints<K>::npos;
const NearestPoints<K> nearest(end_points);
std::vector<std::array<size_t, K>> neighbors(end_points.size());
for (size_t e = 0; e < end_points.size(); ++ e)
neighbors[e] = nearest.query(end_points[e], [e](size_t idx) { return (idx >> 1) != (e >> 1); });
std::array<size_t, K> start_neighbors;
start_neighbors.fill(npos);
if (start_near != nullptr)
start_neighbors = nearest.query(*start_near, [](size_t) { return true; });
// Segments to examine, by id, each queued once.
std::deque<size_t> queue;
std::vector<char> queued(n, 0);
auto enqueue = [&](size_t k) {
if (k < n && ! queued[order[k]]) {
queued[order[k]] = 1;
queue.push_back(order[k]);
}
};
const double min_gain = SCALED_EPSILON;
// Reverses positions i..j if that shortens the chain.
auto try_2opt = [&](size_t i, size_t j) {
if (! reversible(i, j))
return false;
const Vec2d *a = prev_exit(i), *b = next_entry(j);
if (dist(a, &exit(j)) + dist(&entry(i), b) > dist(a, &entry(i)) + dist(&exit(j), b) - min_gain)
return false;
std::reverse(order.begin() + i, order.begin() + j + 1);
for (size_t k = i; k <= j; ++ k)
flipped[order[k]] ^= 1;
refresh(i, j);
for (size_t k : { i - 1, i, j, j + 1 })
enqueue(k);
return true;
};
// Moves positions i..last after position gap (size_t(-1) for the front) if that shortens the chain.
auto try_oropt = [&](size_t i, size_t last, size_t gap) {
if (gap + 1 == i || (gap >= i && gap <= last))
return false;
const Vec2d *a = gap == size_t(-1) ? start_near : &exit(gap);
const Vec2d *b = gap + 1 < n ? &entry(gap + 1) : nullptr;
const double removal_gain = dist(prev_exit(i), &entry(i)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(i), next_entry(last));
const double base = dist(a, b);
const double cost_kept = dist(a, &entry(i)) + dist(&exit(last), b) - base;
const double cost_flipped = reversible(i, last) ? dist(a, &exit(last)) + dist(&entry(i), b) - base : std::numeric_limits<double>::max();
if (std::min(cost_kept, cost_flipped) > removal_gain - min_gain)
return false;
const size_t len = last - i + 1;
// The run lands on first..first+len-1; lo..hi spans every position that changed.
const bool forward = gap > last && gap != size_t(-1);
const size_t first = forward ? gap + 1 - len : gap + 1;
const size_t lo = forward ? i : first;
const size_t hi = forward ? gap : last;
if (forward)
std::rotate(order.begin() + i, order.begin() + last + 1, order.begin() + gap + 1);
else
std::rotate(order.begin() + first, order.begin() + i, order.begin() + last + 1);
if (cost_flipped < cost_kept) {
std::reverse(order.begin() + first, order.begin() + first + len);
for (size_t k = first; k < first + len; ++ k)
flipped[order[k]] ^= 1;
}
refresh(lo, hi);
// The links around the run's new place, and the link closing its old place.
for (size_t k : { first - 1, first, first + len - 1, first + len, forward ? i - 1 : last, forward ? i : last + 1 })
enqueue(k);
return true;
};
// The gap before position k of the chain with positions i..last taken out.
auto gap_before = [](size_t k, size_t i, size_t last) { return k == last + 1 ? i - 1 : k - 1; };
// Applies the first move found that breaks a link next to position p.
// Candidates are tried nearest first, while closer than the link the move would replace at that end point.
auto closer = [&](const Vec2d &at, size_t q, double limit) { return q != npos && (end_points[q] - at).norm() < limit; };
auto improve_at = [&](size_t p) {
// 2-opt breaking the link into p, as the first or the last link of the reversed run.
if (const Vec2d *a = prev_exit(p); a != nullptr) {
const double old_link = dist(a, &entry(p));
for (size_t q : p > 0 ? neighbors[exit_idx(p - 1)] : start_neighbors) {
if (! closer(*a, q, old_link))
break;
if (is_exit(q) && pos[q >> 1] >= p && try_2opt(p, pos[q >> 1]))
return true;
}
if (p > 0)
for (size_t q : neighbors[entry_idx(p)]) {
if (! closer(entry(p), q, old_link))
break;
if (! is_exit(q) && pos[q >> 1] < p && try_2opt(pos[q >> 1], p - 1))
return true;
}
}
// 2-opt breaking the link out of p.
if (p + 1 < n) {
const double old_link = dist(&exit(p), &entry(p + 1));
for (size_t q : neighbors[exit_idx(p)]) {
if (! closer(exit(p), q, old_link))
break;
if (is_exit(q) && pos[q >> 1] > p && try_2opt(p + 1, pos[q >> 1]))
return true;
}
for (size_t q : neighbors[entry_idx(p + 1)]) {
if (! closer(entry(p + 1), q, old_link))
break;
if (! is_exit(q) && pos[q >> 1] <= p && try_2opt(pos[q >> 1], p))
return true;
}
}
// A run of any length that starts or ends next to the start point moves to the front.
if (p == 0)
for (size_t q : start_neighbors) {
if (q == npos)
break;
const size_t m = pos[q >> 1];
if (m == 0)
continue;
// Runs m..k enter next to the start, runs k..m leave there and go flipped.
const bool exit_near = is_exit(q);
for (size_t k = exit_near ? 1 : m; k < (exit_near ? m + 1 : n); ++ k)
if (try_oropt(exit_near ? k : m, exit_near ? m : k, size_t(-1)))
return true;
}
// Or-opt of the runs starting at p: next to an exit a run goes after that segment, next to an entry before it.
for (size_t last = p; last < std::min(p + 3, n); ++ last) {
if (try_oropt(p, last, size_t(-1)))
return true;
const double removal_gain = dist(prev_exit(p), &entry(p)) + dist(&exit(last), next_entry(last)) - dist(prev_exit(p), next_entry(last));
for (size_t e : { entry_idx(p), exit_idx(last) })
for (size_t q : neighbors[e]) {
if (! closer(end_points[e], q, removal_gain))
break;
const size_t m = pos[q >> 1];
if ((m < p || m > last) && try_oropt(p, last, is_exit(q) ? m : gap_before(m, p, last)))
return true;
}
}
return false;
};
// A new link can make room for a run that sits elsewhere, hence the sweeps until one finds no move. Every move shortens
// the chain, so the search ends; the cap only bounds its time.
for (size_t k = 0; k < n; ++ k)
enqueue(k);
bool moved_since_sweep = false;
for (size_t moves = 0; moves < 50 * n;) {
if (queue.empty()) {
if (! moved_since_sweep)
break;
moved_since_sweep = false;
for (size_t k = 0; k < n; ++ k)
enqueue(k);
}
const size_t segment = queue.front();
queue.pop_front();
queued[segment] = 0;
if (improve_at(pos[segment])) {
++ moves;
moved_since_sweep = true;
enqueue(pos[segment]);
}
}
for (size_t k = 0; k < n; ++ k)
chain[k] = { order[k], flipped[order[k]] != 0 };
}
// Chain that always goes on to the nearest free end point, from start.
static std::vector<std::pair<size_t, bool>> chain_nearest_next(const std::vector<Vec2d> &end_points, const std::vector<char> &could_reverse, const Vec2d &start)
{
const NearestPoints<1> nearest(end_points);
std::vector<char> taken(could_reverse.size(), 0);
std::vector<std::pair<size_t, bool>> out;
out.reserve(could_reverse.size());
for (Vec2d at = start; out.size() < could_reverse.size();) {
const size_t e = nearest.query(at, [&](size_t idx) { return ! taken[idx >> 1] && ((idx & 1) == 0 || could_reverse[idx >> 1]); })[0];
taken[e >> 1] = 1;
out.emplace_back(e >> 1, (e & 1) != 0);
at = end_points[e ^ 1];
}
return out;
}
static double chain_travel(const std::vector<std::pair<size_t, bool>> &chain, const std::vector<Vec2d> &end_points, const Vec2d &start)
{
double travel = 0.;
Vec2d at = start;
for (const std::pair<size_t, bool> &segment : chain) {
travel += (end_points[2 * segment.first + segment.second] - at).norm();
at = end_points[2 * segment.first + 1 - segment.second];
}
return travel;
}
template<typename EntityPtr>
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near)
static std::vector<std::pair<size_t, bool>> chain_extrusion_entities_impl(const std::vector<EntityPtr> &entities, const Point *start_near, bool improve = false)
{
auto segment_end_point = [&entities](size_t idx, bool first_point) -> Point { return first_point ? entities[idx]->first_point() : entities[idx]->last_point(); };
auto could_reverse = [&entities](size_t idx) { const ExtrusionEntity *ee = entities[idx]; return ee->is_loop() || ee->can_reverse(); };
std::vector<std::pair<size_t, bool>> out = chain_segments_greedy_constrained_reversals<Point, decltype(segment_end_point), decltype(could_reverse)>(segment_end_point, could_reverse, entities.size(), start_near);
if (improve && out.size() > 1) {
std::vector<Vec2d> end_points;
std::vector<char> reversible;
end_points.reserve(2 * entities.size());
reversible.reserve(entities.size());
for (size_t idx = 0; idx < entities.size(); ++ idx) {
end_points.emplace_back(segment_end_point(idx, true).template cast<double>());
end_points.emplace_back(segment_end_point(idx, false).template cast<double>());
reversible.emplace_back(could_reverse(idx));
}
Vec2d start = Vec2d::Zero();
if (start_near != nullptr)
start = start_near->cast<double>();
if (entities.size() <= 8) {
// Few paths are chained exactly; the local search below can stop short of the shortest chain.
out = chain_segments_exact(end_points, reversible, start_near != nullptr ? &start : nullptr);
} else {
improve_chain_by_local_search(out, end_points, reversible, start_near != nullptr ? &start : nullptr);
if (start_near != nullptr) {
// The greedy chain can settle into a sweep that local moves cannot undo, which a nearest-next chain may avoid.
std::vector<std::pair<size_t, bool>> nearest_next = chain_nearest_next(end_points, reversible, start);
if (chain_travel(nearest_next, end_points, start) < chain_travel(out, end_points, start) - SCALED_EPSILON) {
improve_chain_by_local_search(nearest_next, end_points, reversible, &start);
out = std::move(nearest_next);
}
}
}
}
for (std::pair<size_t, bool> &segment : out) {
const ExtrusionEntity *ee = entities[segment.first];
if (ee->is_loop())
@@ -1086,7 +1500,8 @@ void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const s
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near)
{
chain_and_reorder_extrusion_entities(entities, &start_near);
remove_entities_without_endpoints(entities);
reorder_extrusion_entities(entities, chain_extrusion_entities_impl(entities, &start_near, true));
}
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near)
@@ -1101,7 +1516,7 @@ void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &e
remove_entities_without_endpoints(entities);
std::vector<const ExtrusionEntity*> out;
out.reserve(entities.size());
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near)) {
for (const auto &[idx, reverse] : chain_extrusion_entities_impl(entities, &start_near, true)) {
if (reverse) {
ExtrusionEntity *clone = reversed_clones.emplace_back(entities[idx]->clone()).get();
clone->reverse();
@@ -1159,16 +1574,74 @@ std::vector<size_t> chain_points(const Points &points, const Point *start_near)
return out;
}
// Moves runs of up to three points of the cycle next to one of the 8 points nearest to an end of the run, reversed or not,
// while that shortens the cycle. path[0] stays first. Returns true if any move was made.
static bool improve_cycle_by_or_opt(std::vector<size_t> &path, const Points &points)
{
const size_t pn = path.size();
if (pn < 5)
return false;
std::vector<Vec2d> coords(points.size());
for (size_t i = 0; i < points.size(); ++ i)
coords[i] = points[i].cast<double>();
const NearestPoints<8> grid(coords);
std::vector<size_t> pos(points.size());
auto refresh = [&]() { for (size_t k = 0; k < pn; ++ k) pos[path[k]] = k; };
refresh();
auto dist = [&](size_t a, size_t b) { return (coords[path[a % pn]] - coords[path[b % pn]]).norm(); };
bool improved = false;
for (bool moved = true; moved;) {
moved = false;
for (size_t len = 1; len <= 3; ++ len)
for (size_t i = 1; i + len <= pn; ++ i) {
// The run path[i..last] goes between path[k] and path[k + 1].
const size_t last = i + len - 1;
const double removal = dist(i - 1, i) + dist(last, last + 1) - dist(i - 1, last + 1);
bool applied = removal <= SCALED_EPSILON;
for (size_t end : { path[i], path[last] })
for (size_t q : grid.query(coords[end], [end](size_t idx) { return idx != end; }))
for (size_t k : { q == NearestPoints<8>::npos ? pn : (pos[q] + pn - 1) % pn, q == NearestPoints<8>::npos ? pn : pos[q] }) {
if (applied || k == pn || (k + 1 >= i && k <= last))
continue;
const double base = dist(k, k + 1);
const double kept = dist(k, i) + dist(last, k + 1) - base;
const double reversed = dist(k, last) + dist(i, k + 1) - base;
if (std::min(kept, reversed) > removal - SCALED_EPSILON)
continue;
std::vector<size_t> run(path.begin() + i, path.begin() + last + 1);
if (reversed < kept)
std::reverse(run.begin(), run.end());
path.erase(path.begin() + i, path.begin() + last + 1);
path.insert(path.begin() + (k < i ? k + 1 : k + 1 - len), run.begin(), run.end());
refresh();
applied = moved = improved = true;
}
}
}
return improved;
}
std::vector<size_t> chain_points_with_postprocessing(const Points &points, const Point *start_near)
{
std::vector<size_t> path = chain_points(points, start_near);
// Alternate 2-opt and crossing removal until convergence.
// 2-opt can create new crossings, and crossing removal can create new
// opportunities for 2-opt improvement. Break early if neither improves.
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, points);
improved |= tsp_remove_crossings(path, points);
if (!improved) break;
std::vector<size_t> path;
if (points.size() <= 10) {
// Orca: few points are toured exactly, from the point nearest start_near toward its nearer neighbor.
path = tsp_exact_cycle(points);
if (start_near != nullptr && path.size() > 2) {
auto dist = [&points](size_t a, const Point &b) { return (points[a] - b).cast<double>().squaredNorm(); };
std::rotate(path.begin(), std::min_element(path.begin(), path.end(), [&](size_t a, size_t b) { return dist(a, *start_near) < dist(b, *start_near); }), path.end());
if (dist(path.back(), points[path.front()]) < dist(path[1], points[path.front()]))
std::reverse(path.begin() + 1, path.end());
}
} else {
path = chain_points(points, start_near);
// Alternate 2-opt, or-opt and crossing removal: each can open moves for the others. Break early if none improves.
for (int iter = 0; iter < 3; ++iter) {
bool improved = tsp_2opt_improve(path, points);
improved |= improve_cycle_by_or_opt(path, points);
improved |= tsp_remove_crossings(path, points);
if (!improved) break;
}
}
if (start_near == nullptr)
tsp_rotate_minimize_closing(path, points);
+4 -1
View File
@@ -23,9 +23,12 @@ std::vector<size_t> chain_expolygons(const ExPolygons &input_exploy);
std::vector<std::pair<size_t, bool>> chain_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
void reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const std::vector<std::pair<size_t, bool>> &chain);
// From start_near, the greedy chain is refined by 2-opt and or-opt moves, which shortens the travel at some extra cost.
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point &start_near);
// The greedy chain only.
void chain_and_reorder_extrusion_entities(std::vector<ExtrusionEntity*> &entities, const Point *start_near = nullptr);
// Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the originals stay unchanged.
// Refined as above. Each entity the chain reverses is replaced by a reversed clone that reversed_clones owns, so the
// originals stay unchanged.
void chain_and_reorder_extrusion_entities(std::vector<const ExtrusionEntity*> &entities, const Point &start_near,
std::vector<std::unique_ptr<ExtrusionEntity>> &reversed_clones);
+8 -8
View File
@@ -843,17 +843,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
// The sparse infill density is the surface density of the adaptive TPMS infill; at 100% the infill is solid.
bool have_tpms_infill = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
is_tpms_adaptive_pattern(pattern);
toggle_line("tpms_adaptive", have_tpms_infill);
bool have_tpms_adaptive = have_tpms_infill && config->opt_enum<TpmsAdaptiveMode>("tpms_adaptive") != TpmsAdaptiveMode::Disabled;
toggle_line("tpms_interior_density", have_tpms_adaptive);
toggle_line("tpms_adaptive_gradient", have_tpms_adaptive);
// The sparse infill density is the surface density of the adaptive infill; at 100% the infill is solid.
bool have_adaptive_infill_pattern = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
is_adaptive_infill_pattern(pattern);
toggle_line("adaptive_infill_mode", have_adaptive_infill_pattern);
bool have_adaptive_infill = have_adaptive_infill_pattern && config->opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") != AdaptiveInfillMode::Disabled;
toggle_line("adaptive_infill_interior_density", have_adaptive_infill);
toggle_line("adaptive_infill_gradient", have_adaptive_infill);
// gyroid_optimized only applies when the sparse infill pattern is gyroid without adaptive density;
// hide the whole line otherwise.
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_tpms_adaptive);
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_adaptive_infill);
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
if (have_infill) {
+1 -1
View File
@@ -161,7 +161,7 @@ void DailyTipsDataRenderer::render_img(const ImVec2& start_pos, const ImVec2& si
bool has_cjk(const std::string &text)
{
for (size_t i = 0; i < text.size() - 2; ++i) {
for (size_t i = 0; i + 2 < text.size(); ++i) {
unsigned char c1 = text[i];
unsigned char c2 = text[i + 1];
unsigned char c3 = text[i + 2];
+5 -3
View File
@@ -149,9 +149,9 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
{"sparse_infill_density", "", 1},
{"fill_multiline", "", 1},
{"sparse_infill_pattern", "", 1},
{"tpms_adaptive", "", 1},
{"tpms_interior_density", "", 1},
{"tpms_adaptive_gradient", "", 1},
{"adaptive_infill_mode", "", 1},
{"adaptive_infill_interior_density", "", 1},
{"adaptive_infill_gradient", "", 1},
{"sparse_infill_smooth_factor", "", 1},
{"lateral_lattice_angle_1", "", 1},
{"lateral_lattice_angle_2", "", 1},
@@ -2158,6 +2158,7 @@ wxMenu* MenuFactory::multi_selection_menu()
menu->AppendSeparator();
append_menu_items_convert_unit(menu);
append_menu_item_reload_from_disk(menu);
append_menu_item_replace_all_with_stl(menu);
//BBS
append_menu_item_change_filament(menu);
@@ -2173,6 +2174,7 @@ wxMenu* MenuFactory::multi_selection_menu()
//append_menu_item_simplify(menu);
append_menu_item_delete(menu);
append_menu_items_convert_unit(menu);
append_menu_item_reload_from_disk(menu);
append_menu_item_replace_all_with_stl(menu);
append_menu_item_change_filament(menu);
wxMenu* split_menu = new wxMenu();
+2 -2
View File
@@ -1089,8 +1089,8 @@ EmbossStyles GLGizmoEmboss::create_default_styles()
styles.push_back(WxFontUtils::create_emboss_style(wx_font, _u8L("First font")));
} else {
// On current OS is not installed any correct TTF font
// use font packed with Slic3r
std::string font_path = Slic3r::resources_dir() + "/fonts/NotoSans-Regular.ttf";
// use font packed with OrcaSlicer
std::string font_path = Slic3r::resources_dir() + "/fonts/HarmonyOS_Sans_SC_Regular.ttf";
styles.push_back(EmbossStyle{_u8L("Default font"), font_path, EmbossStyle::Type::file_path});
}
return styles;
+1 -1
View File
@@ -33,7 +33,7 @@ static const char* HMS_LOCAL_IMG_PATH = "hms/local_image";
// the local HMS info
// Orca: dev-id-type set trimmed to the devices Orca ships local HMS images for
static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E"};
static std::unordered_set<std::string> package_dev_id_types {"094", "239", "093", "22E", "20P", "26A", "31B"};
// Orca: HMS should be disabled when stealth mode is on or networking is not installed
static bool should_disable_hms()
-2
View File
@@ -716,8 +716,6 @@ void PrintJob::process(Ctl &ctl)
} else {
// wait for printer mqtt ready the same job id
wxGetApp().plater()->record_slice_preset("print");
BOOST_LOG_TRIVIAL(error) << "print_job: send ok.";
wxCommandEvent* evt = new wxCommandEvent(m_print_job_completed_id);
if (!m_completed_evt_data.empty())
+52 -72
View File
@@ -228,6 +228,7 @@
#include "Jobs/PlaterWorker.hpp"
#include "Jobs/BoostThreadWorker.hpp"
#include "BackgroundSlicingProcess.hpp"
#include "PostProcessor.hpp"
#include "SelectMachine.hpp"
#include "SendMultiMachinePage.hpp"
#include "SendToPrinter.hpp"
@@ -17865,6 +17866,9 @@ void Plater::calib_input_shaping_freq(const Calib_Params& params)
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
print_config->set_key_value("layer_height", new ConfigOptionFloat(0.2));
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
@@ -17929,6 +17933,9 @@ void Plater::calib_input_shaping_damp(const Calib_Params& params)
filament_config->set_key_value("slow_down_min_speed", new ConfigOptionFloats { 0.0 });
filament_config->set_key_value("slow_down_for_layer_cooling", new ConfigOptionBools{false});
set_config_values<bool, ConfigOptionBools>(print_config, "enable_overhang_speed", false);
// A 100% factor sends Klipper MINIMUM_CRUISE_RATIO=0 with every acceleration change, for the whole test.
print_config->set_key_value("accel_to_decel_enable", new ConfigOptionBool(true));
print_config->set_key_value("accel_to_decel_factor", new ConfigOptionPercent(100));
print_config->set_key_value("timelapse_type", new ConfigOptionEnum<TimelapseType>(tlTraditional));
print_config->set_key_value("wall_loops", new ConfigOptionInt(1));
print_config->set_key_value("top_shell_layers", new ConfigOptionInt(0));
@@ -18670,7 +18677,7 @@ void ProjectDropDialog::on_dpi_changed(const wxRect& suggested_rect)
//BBS: remove GCodeViewer as seperate APP logic
bool Plater::load_files(const wxArrayString& filenames)
{
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc)", std::regex::icase);
const std::regex pattern_drop(".*[.](stp|step|stl|oltp|obj|amf|3mf|svg|zip|drc|glb|gltf|fbx)", std::regex::icase);
const std::regex pattern_gcode_drop(".*[.](gcode|g)", std::regex::icase);
std::vector<fs::path> normal_paths;
@@ -19572,7 +19579,11 @@ void Plater::export_gcode_3mf(bool export_all)
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
if (wxGetApp().app_config->get_bool("export_statistics"))
strategy = strategy | SaveStrategy::WithStatistics;
export_3mf(output_path, strategy, plate_idx);
if (export_3mf(output_path, strategy, plate_idx) < 0) {
// No file was written (e.g. a post-processing script failed), don't report a successful export.
p->exporting_status = ExportingStatus::NOT_EXPORTING;
return;
}
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
@@ -20429,6 +20440,45 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
strategy & SaveStrategy::WithStatistics);
// Orca: non-BBL G-code isn't post-processed in place, so run the scripts on a copy before packing it.
std::vector<std::string> pp_tmp_files;
ScopeGuard remove_pp_tmp_files([&pp_tmp_files]() {
for (const std::string& f : pp_tmp_files) {
boost::system::error_code ec;
boost::filesystem::remove(f, ec);
}
});
// Backups (SkipStatic) don't pack G-code, so don't run user scripts for them.
if ((strategy & SaveStrategy::WithGcode) && !(strategy & SaveStrategy::SkipStatic) &&
p->printer_technology == ptFFF && !wxGetApp().preset_bundle->is_bbl_vendor()) {
try {
for (PlateData* pd : plate_data_list) {
if (pd == nullptr || pd->gcode_file.empty())
continue;
PartPlate* plate = p->partplate_list.get_plate(pd->plate_index);
if (plate == nullptr)
continue;
PrintBase* print = nullptr;
plate->get_print(&print, nullptr, nullptr);
Print* fff_print = dynamic_cast<Print*>(print);
if (fff_print == nullptr)
continue;
std::string src_path = pd->gcode_file; // in/out: becomes the post-processed copy if a script ran
std::string output_name = path_u8; // scripts see the real target; a rename is ignored for 3mf
if (run_post_process_scripts(src_path, true, "File", output_name, fff_print->full_print_config())) {
pp_tmp_files.push_back(src_path);
pd->gcode_file = src_path;
}
}
} catch (const std::exception& ex) {
release_PlateData_list(plate_data_list);
show_error(this, ex.what(), false);
return -1;
}
}
// BBS: backup
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
std::vector<Preset*> project_presets = preset_bundle.get_current_project_embedded_presets();
@@ -20746,76 +20796,6 @@ void Plater::reslice()
p->preview->reload_print();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": finished, started slicing for plate %1%") % p->partplate_list.get_curr_plate_index();
record_slice_preset("slicing");
}
void Plater::record_slice_preset(std::string action)
{
// record slice preset
try
{
json j;
auto printer_preset = wxGetApp().preset_bundle->printers.get_edited_preset_with_vendor_profile().preset;
if (printer_preset.is_system) {
j["printer_preset_name"] = printer_preset.name;
}
else {
j["printer_preset_name"] = printer_preset.config.opt_string("inherits");
}
const t_config_enum_values* keys_map = print_config_def.get("curr_bed_type")->enum_keys_map;
if (keys_map) {
for (auto item : *keys_map) {
if (item.second == wxGetApp().preset_bundle->project_config.opt_enum<BedType>("curr_bed_type")) {
j["curr_bed_type"] = item.first;
break;
}
}
}
auto filament_presets = wxGetApp().preset_bundle->filament_presets;
for (int i = 0; i < filament_presets.size(); ++i) {
auto filament_preset = wxGetApp().preset_bundle->filaments.find_preset(filament_presets[i]);
if (filament_preset->is_system) {
j["filament_preset_" + std::to_string(i)] = filament_preset->name;
}
else {
j["filament_preset_" + std::to_string(i)] = filament_preset->config.opt_string("inherits");
}
}
Preset& print_preset = wxGetApp().preset_bundle->prints.get_edited_preset();
if (print_preset.is_system) {
j["process_preset"] = print_preset.name;
}
else {
j["process_preset"] = print_preset.config.opt_string("inherits");
}
j["support_type"] = ConfigOptionEnum<SupportType>::get_enum_names().at(print_preset.config.opt_enum<SupportType>("support_type"));
j["sparse_infill_pattern"] = ConfigOptionEnum<InfillPattern>::get_enum_names().at(print_preset.config.opt_enum<InfillPattern>("sparse_infill_pattern"));
j["sparse_infill_density"] = print_preset.config.opt<ConfigOptionPercent>("sparse_infill_density")->value;
j["brim_type"] = ConfigOptionEnum<BrimType>::get_enum_names().at(print_preset.config.opt_enum<BrimType>("brim_type"));
j["user_mode"] = wxGetApp().get_mode_str();
if (p->background_process.fff_print()) {
const DynamicPrintConfig& full_config = p->background_process.fff_print()->full_print_config();
json values = json::array();
if (full_config.has("different_settings_to_system")) {
std::vector<std::string> different_values = full_config.option<ConfigOptionStrings>("different_settings_to_system")->values;
for (auto& item : different_values) {
values.push_back(item);
}
}
j["different_settings_to_system"] = values;
}
j["record_event"] = action;
NetworkAgent* agent = wxGetApp().getAgent();
}
catch (...)
{
return;
}
}
//BBS: add project slicing related logic
-1
View File
@@ -597,7 +597,6 @@ public:
bool has_toolpaths_to_export() const;
void export_toolpaths_to_obj() const;
void reslice();
void record_slice_preset(std::string action);
void reslice_SLA_supports(const ModelObject &object, bool postpone_error_messages = false);
void reslice_SLA_hollowing(const ModelObject &object, bool postpone_error_messages = false);
void reslice_SLA_until_step(SLAPrintObjectStep step, const ModelObject &object, bool postpone_error_messages = false);
+7 -3
View File
@@ -5738,8 +5738,9 @@ void SelectMachineDialog::reset_and_sync_ams_list()
}
// Orca: collapse the per-nozzle material items into the single panel when the printer has one
// extruder or a filament switch (both feed a single logical mapping surface); otherwise keep the
// left/right split. Early-returns unless an item is actually in the wrong panel.
// extruder or a filament switch (both feed a single logical mapping surface), or when the plate is
// sliced for one nozzle (it has no left/right filament map); otherwise keep the left/right split.
// Early-returns unless an item is actually in the wrong panel.
void SelectMachineDialog::update_material_item_pos(MachineObject* obj_)
{
if (!obj_) {
@@ -5748,7 +5749,10 @@ void SelectMachineDialog::update_material_item_pos(MachineObject* obj_)
const bool is_single_head = obj_->GetExtderSystem()->GetTotalExtderCount() < 2;
const bool has_switcher = obj_->GetFilaSwitch()->IsInstalled();
const bool use_single_panel = is_single_head || has_switcher;
// reset_and_sync_ams_list() fills m_filaments_map only for a multi-nozzle plate
const auto& full_config = wxGetApp().preset_bundle->full_config();
const bool single_nozzle_plate = full_config.option<ConfigOptionFloats>("nozzle_diameter")->values.size() < 2;
const bool use_single_panel = is_single_head || has_switcher || single_nozzle_plate;
bool to_change_pos = false;
for (const auto& iter : m_materialList) {
+3 -3
View File
@@ -2968,9 +2968,9 @@ void TabPrint::build()
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("tpms_adaptive", "strength_settings_patterns#adaptive-density");
optgroup->append_single_option_line("tpms_interior_density", "strength_settings_patterns#interior-density");
optgroup->append_single_option_line("tpms_adaptive_gradient", "strength_settings_patterns#adaptive-gradient");
optgroup->append_single_option_line("adaptive_infill_mode", "strength_settings_patterns#adaptive-density");
optgroup->append_single_option_line("adaptive_infill_interior_density", "strength_settings_patterns#interior-density");
optgroup->append_single_option_line("adaptive_infill_gradient", "strength_settings_patterns#adaptive-gradient");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
+11 -4
View File
@@ -7,6 +7,8 @@
#include "slic3r/GUI/GUI_App.hpp"
#include "slic3r/GUI/DeviceCore/DevManager.h"
#include <algorithm>
#include <array>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
@@ -16,6 +18,7 @@
#include <cmath>
#include <slic3r/GUI/DeviceManager.hpp>
#include <string>
#include <string_view>
#include <utility>
#include <vector>
using json = nlohmann::json;
@@ -64,12 +67,16 @@ const BambuFilamentIdMap& bambu_filament_id_map()
return map;
}
// Rewrites every string under "tray_info_idx", "filament_id" or "filamentId", at any depth, in place.
// Keys whose string value is a filament id. "cate" and "fila_id" are the H2C nozzle rack's: the
// auto nozzle mapping request and the rack status in the push message.
constexpr std::array<std::string_view, 5> filament_id_keys{"tray_info_idx", "filament_id", "filamentId", "cate", "fila_id"};
// Rewrites every string under a filament id key, at any depth, in place.
void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::string>& map)
{
if (j.is_object()) {
for (auto& [key, value] : j.items()) {
if (value.is_string() && (key == "tray_info_idx" || key == "filament_id" || key == "filamentId")) {
if (value.is_string() && std::find(filament_id_keys.begin(), filament_id_keys.end(), key) != filament_id_keys.end()) {
auto it = map.find(value.get_ref<const std::string&>());
if (it != map.end())
value = it->second;
@@ -84,8 +91,8 @@ void rewrite_filament_ids(json& j, const std::unordered_map<std::string, std::st
// Text that does not parse as JSON, or that mentions none of the id keys, comes back byte-identical.
std::string rewrite_filament_ids(std::string text, const std::unordered_map<std::string, std::string>& map)
{
if (map.empty() || (text.find("tray_info_idx") == std::string::npos && text.find("filament_id") == std::string::npos &&
text.find("filamentId") == std::string::npos))
if (map.empty() || std::none_of(filament_id_keys.begin(), filament_id_keys.end(),
[&text](std::string_view key) { return text.find(key) != std::string::npos; }))
return text; // nothing to map, skip the parse (moved, not copied)
try {
json j = json::parse(text);
+2 -10
View File
@@ -1415,16 +1415,8 @@ void MoonrakerPrinterAgent::announce_printhost_device()
}
}
// Try to fetch actual device name from Moonraker
// Priority: 1) Moonraker hostname, 2) Preset model name, 3) Generic fallback
std::string dev_name;
MoonrakerDeviceInfo info;
std::string fetch_error;
if (fetch_device_info(device_info.base_url, device_info.api_key, info, fetch_error) && !info.dev_name.empty()) {
dev_name = info.dev_name;
} else {
dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
}
// Never asks the printer, since discovery runs on the GUI thread and an unreachable printer would freeze it
const std::string dev_name = device_info.model_name.empty() ? "Moonraker Printer" : device_info.model_name;
const std::string model_id = device_info.model_id;
+14 -5
View File
@@ -1917,13 +1917,20 @@ RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& rea
bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason)
{
bool needs_refresh = false;
{
const auto needs_refresh = [&] {
std::lock_guard<std::mutex> lock(session_mutex);
needs_refresh = should_refresh_locked(skew);
}
return should_refresh_locked(skew);
};
if (!needs_refresh())
return true;
if (!needs_refresh)
// A refresh may already be running on another thread, e.g. the one start() launches. Wait for
// it and check again instead of spending the refresh token it is rotating. Bounded: the
// refresh POST times out after 30 s.
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(60);
while (refresh_running.load() && std::chrono::steady_clock::now() < deadline)
std::this_thread::sleep_for(std::chrono::milliseconds(10));
if (!needs_refresh())
return true;
// First attempt. refresh_from_storage blocks on the cross-process lock and reads the
@@ -1935,6 +1942,8 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
// One retry, only for a transient network failure of the refresh POST (lost response,
// timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store.
std::this_thread::sleep_for(std::chrono::milliseconds(750));
if (!needs_refresh())
return true;
return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
}
+10 -3
View File
@@ -11,6 +11,7 @@
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#include <cstddef>
#include <future>
#include <memory>
#include <ios>
#include <slic3r/plugin/PluginDescriptor.hpp>
@@ -49,16 +50,22 @@ bool CloudPluginService::fetch_manifests_into_descriptors(std::vector<PluginDesc
unauthorized.clear();
if (m_orca_agent) {
// Refresh the token once, then run both requests in parallel.
m_orca_agent->ensure_token_fresh("fetch_plugin_manifests");
std::vector<PluginDescriptor> mine_descriptors;
auto mine = std::async(std::launch::async, [agent = m_orca_agent, &mine_descriptors] {
return agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
});
int ret = m_orca_agent->fetch_subscribed_manifests_into_descriptors(descriptors, not_found, unauthorized);
const int mine_ret = mine.get();
if (ret != 0) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get subscribed plugins.";
return false;
}
std::vector<PluginDescriptor> mine_descriptors;
ret = m_orca_agent->fetch_mine_manifests_into_descriptors(mine_descriptors);
ret = mine_ret;
if (ret != 0) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": Failed to get owned plugins.";
+132 -383
View File
@@ -11,7 +11,6 @@
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstddef>
#include <cstdlib>
@@ -131,20 +130,6 @@ static AuditEventCategory event_category(const std::string& event_name)
return it == audit_event_categories.end() ? AuditEventCategory::None : it->second;
}
// Events the 3.12 stdlib raises with sys.audit() inside the function that performs the whole
// operation, so the raising frame bounds everything the operation does. Every other mapped event
// is a primitive raised from C, whose current frame is whatever Python code called it -- possibly
// a library loop over many targets -- so it never marks an operation. glob.glob and glob.glob/2
// are raised in iglob(), which returns its lazy generator before any directory is listed.
static const std::unordered_set<std::string> operation_root_events{
"urllib.Request", "http.client.connect", "os.walk", "os.fwalk",
"glob.glob", "glob.glob/2", "pathlib.Path.glob", "pathlib.Path.rglob",
"subprocess.Popen", "pty.spawn", "tempfile.mkstemp", "tempfile.mkdtemp",
"shutil.copyfile", "shutil.copymode", "shutil.copystat", "shutil.copytree",
"shutil.chown", "shutil.move", "shutil.rmtree", "shutil.make_archive",
"shutil.unpack_archive",
};
// True for the categories whose targets are filesystem paths, as opposed to a network
// address or a process command line -- the deny-path and allowed-root checks only make sense
// against a path.
@@ -161,24 +146,6 @@ static bool is_fs_category(AuditEventCategory category)
}
}
// The category's name in a saved event grant, e.g. "FsRead" in "FsRead:open".
static const char* category_name(AuditEventCategory category)
{
switch (category) {
case AuditEventCategory::FsRead: return "FsRead";
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
case AuditEventCategory::FsCreate: return "FsCreate";
case AuditEventCategory::FsDelete: return "FsDelete";
case AuditEventCategory::Http: return "Http";
case AuditEventCategory::Socket: return "Socket";
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
case AuditEventCategory::Threading: return "Threading";
case AuditEventCategory::None: break;
}
return "None";
}
// ---------------------------------------------------------------------------
// Path safety
// ---------------------------------------------------------------------------
@@ -248,50 +215,26 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local std::string PluginAuditManager::m_current_capability_name = "";
thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
thread_local std::vector<PyObject*> PluginAuditManager::m_root_frames;
thread_local int PluginAuditManager::m_releasing_frames = 0;
thread_local bool PluginAuditManager::m_audit_denial_pending = false;
thread_local bool PluginAuditManager::m_has_last_violation = false;
thread_local AuditViolation PluginAuditManager::m_last_violation;
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key,
const std::string& capability_name)
: m_plugin_key(plugin_key)
, m_capability(capability_name)
, m_previous_id(PluginAuditManager::instance().current_plugin())
: m_previous_id(PluginAuditManager::instance().current_plugin())
, m_previous_capability(PluginAuditManager::instance().current_capability())
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
, m_previous_root_frames(std::move(PluginAuditManager::m_root_frames))
{
PluginAuditManager::instance().set_current_plugin(plugin_key);
PluginAuditManager::instance().set_current_capability(capability_name);
PluginAuditManager::m_scoped_allowed_roots.clear();
PluginAuditManager::m_root_frames.clear();
}
ScopedPluginAuditContext::~ScopedPluginAuditContext()
{
PluginAuditManager& mgr = PluginAuditManager::instance();
// Let this scope's root frames go while it is still the current context, and as its own
// plugin: the trampoline's denial path clears the key before the scope ends, and a finalizer
// must never be judged as the outer plugin, or bypass the audit when there is none. A
// finalizer may record a new root frame, so repeat until none is held.
if (!PluginAuditManager::m_root_frames.empty()) {
assert(PyGILState_Check());
mgr.set_current_plugin(m_plugin_key);
mgr.set_current_capability(m_capability);
while (!PluginAuditManager::m_root_frames.empty()) {
std::vector<PyObject*> frames = std::move(PluginAuditManager::m_root_frames);
PluginAuditManager::m_root_frames.clear();
mgr.release_frames(frames);
}
}
mgr.set_current_plugin(m_previous_id);
mgr.set_current_capability(m_previous_capability);
PluginAuditManager::instance().set_current_plugin(m_previous_id);
PluginAuditManager::instance().set_current_capability(m_previous_capability);
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
PluginAuditManager::m_root_frames = std::move(m_previous_root_frames);
}
// ---------------------------------------------------------------------------
@@ -498,12 +441,6 @@ AuditDecision PluginAuditManager::check_path_access(const boost::filesystem::pat
candidate = absolute_candidate;
}
// Importing Python modules reads their source and may inspect package directories. These
// reads are normal interpreter work, not plugin data access; keep the exemption read-only
// and exclude third-party packages from the stdlib roots.
if (!is_write && is_stdlib_path(candidate))
return {true, ""};
// A root that doesn't allow writes only matches a read-shaped request; a write/create/
// delete-shaped one falls through to "outside allowed root" for that root even though the
// path is physically inside it.
@@ -638,6 +575,34 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const
return true;
}
bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids) const
{
if (plugin_key.empty() || call_site_ids.empty())
return false;
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_approved_call_sites.find(plugin_key);
if (it == m_approved_call_sites.end())
return false;
for (const auto& id : call_site_ids)
if (it->second.count(id))
return true;
return false;
}
void PluginAuditManager::record_approved_call_sites(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids)
{
if (plugin_key.empty() || call_site_ids.empty())
return;
std::lock_guard<std::mutex> lock(m_mutex);
auto& approved = m_approved_call_sites[plugin_key];
approved.insert(call_site_ids.begin(), call_site_ids.end());
}
// ---------------------------------------------------------------------------
// The C-level audit hook
// ---------------------------------------------------------------------------
@@ -698,88 +663,40 @@ std::string python_str(PyObject* object)
return result;
}
// The interpreter's stdlib folders, and the site-packages folders inside them that hold
// third-party code, both resolved. Set by install_hook(); like the verdict cache below, only
// touched while the GIL is held.
static std::vector<boost::filesystem::path> s_stdlib_dirs;
static std::vector<boost::filesystem::path> s_site_package_dirs;
static std::unordered_map<std::string, bool> s_stdlib_file_verdicts; // co_filename -> is stdlib
void collect_stdlib_dirs()
std::vector<std::string> call_site_identities(const std::string& plugin_root)
{
s_stdlib_dirs.clear();
s_site_package_dirs.clear();
s_stdlib_file_verdicts.clear();
std::vector<std::string> ids;
if (plugin_root.empty())
return ids;
PyObject* sysconfig = PyImport_ImportModule("sysconfig");
PyObject* paths = sysconfig ? PyObject_CallMethod(sysconfig, "get_paths", nullptr) : nullptr;
if (paths && PyDict_Check(paths)) {
const auto collect = [paths](const char* key, std::vector<boost::filesystem::path>& out) {
const std::string dir = python_unicode(PyDict_GetItemString(paths, key)); // borrowed
if (dir.empty())
return;
boost::system::error_code ec;
const boost::filesystem::path resolved = boost::filesystem::weakly_canonical(dir, ec);
out.push_back(ec ? boost::filesystem::path(dir) : resolved);
};
collect("stdlib", s_stdlib_dirs);
collect("platstdlib", s_stdlib_dirs);
collect("purelib", s_site_package_dirs);
collect("platlib", s_site_package_dirs);
}
Py_XDECREF(paths);
Py_XDECREF(sysconfig);
if (PyErr_Occurred()) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to read the stdlib folders from sysconfig";
}
}
bool is_stdlib_frame(PyObject* frame)
{
PyCodeObject* code = PyFrame_GetCode(reinterpret_cast<PyFrameObject*>(frame)); // new reference
const std::string filename = python_unicode(code->co_filename);
Py_DECREF(code);
return PluginAuditManager::is_stdlib_file(filename);
}
// The frames on this thread's Python stack, innermost first. The pointers stay valid while the
// caller runs, because a running activation keeps its frame object alive.
std::vector<PyObject*> stack_frames()
{
std::vector<PyObject*> frames;
PyFrameObject* frame = PyEval_GetFrame(); // borrowed reference
Py_XINCREF(frame); // normalize to an owned reference for the loop below
while (frame) {
frames.push_back(reinterpret_cast<PyObject*>(frame));
PyCodeObject* code = PyFrame_GetCode(frame); // new reference
const std::string filename = python_unicode(reinterpret_cast<PyObject*>(code->co_filename));
const bool is_plugin_frame = !filename.empty() && boost::algorithm::starts_with(filename, plugin_root);
std::string id;
if (!is_plugin_frame && !filename.empty()) {
const std::string funcname = python_unicode(reinterpret_cast<PyObject*>(code->co_name));
id = filename + ":" + funcname + ":" + std::to_string(code->co_firstlineno);
}
Py_DECREF(code);
PyFrameObject* back = PyFrame_GetBack(frame); // new reference, or nullptr at the top of the stack
Py_DECREF(frame);
frame = back;
if (is_plugin_frame)
break;
if (!id.empty())
ids.push_back(std::move(id));
}
return frames;
}
// Moves the held root frames whose operation has returned from `held` to `dropped`. A suspended
// generator's frame (os.walk between two items) is off the stack, but its operation resumes, so
// it stays held until the scope ends.
void take_returned_frames(std::vector<PyObject*>& held, std::vector<PyObject*>& dropped)
{
if (held.empty())
return;
Py_XDECREF(frame); // only holds a reference here if the loop exited via break
const std::vector<PyObject*> stack = stack_frames();
const auto running = [&stack](PyObject* frame) {
if (std::find(stack.begin(), stack.end(), frame) != stack.end())
return true;
// NULL once the generator has finished or been deleted: its frame then owns its own data.
PyObject* generator = PyFrame_GetGenerator(reinterpret_cast<PyFrameObject*>(frame)); // new reference
const bool suspended = generator != nullptr;
Py_XDECREF(generator);
return suspended;
};
const auto returned = std::stable_partition(held.begin(), held.end(), running);
dropped.insert(dropped.end(), returned, held.end());
held.erase(returned, held.end());
return ids;
}
static const std::unordered_set<std::string> two_path_fs_events{
@@ -800,15 +717,12 @@ static const std::unordered_map<std::string, std::vector<Py_ssize_t>> audit_targ
{"socket.getnameinfo", {0}},
{"socket.getservbyname", {0}},
{"socket.getservbyport", {0}},
{"socket.sendto", {1}},
{"socket.sendmsg", {1}},
{"os.system", {0}},
{"subprocess.Popen", {1, 0}},
{"os.posix_spawn", {1, 0}},
{"os.spawn", {2, 1}},
{"os.startfile", {0}},
{"os.startfile/2", {0}},
{"pty.spawn", {0}},
{"_winapi.CreateProcess", {1, 0}},
{"_posixsubprocess.fork_exec", {0}},
@@ -853,11 +767,7 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
const auto it = audit_target_arg_indices.find(event_name);
if (it != audit_target_arg_indices.end()) {
for (const Py_ssize_t index : it->second) {
// None is an argument left out (sendmsg without an address), not a target named "None".
PyObject* const item = tuple_item(args, index);
if (item == Py_None)
continue;
std::string value = python_str(item);
std::string value = python_str(tuple_item(args, index));
if (!value.empty()) {
targets.push_back(std::move(value));
break;
@@ -868,14 +778,8 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
return targets;
}
// The saved list that grants an event: its category's target list, or the event-grant list for
// an event with no target. Kept apart, so that no grant doubles as a target of the same name (an
// os.fork grant is not an os.exec of a command named "os.fork").
std::vector<std::string>* permission_list_for(AuditEventCategory category, bool by_event, PluginPermissions& permissions)
std::vector<std::string>* permission_list_for(AuditEventCategory category, PluginPermissions& permissions)
{
if (by_event)
return &permissions.events;
switch (category) {
case AuditEventCategory::FsRead: return &permissions.fs_read;
case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite;
@@ -888,13 +792,6 @@ std::vector<std::string>* permission_list_for(AuditEventCategory category, bool
}
}
// An event with no target is granted by its effective category and name, e.g. "FsRead:open"
// for a read of a file descriptor, which does not grant a write of one ("FsReadWrite:open").
std::string event_grant_key(AuditEventCategory category, const std::string& event_name)
{
return std::string(category_name(category)) + ":" + event_name;
}
bool has_permission(const std::vector<std::string>& granted, const std::string& target)
{
return std::find(granted.begin(), granted.end(), target) != granted.end();
@@ -1005,41 +902,22 @@ bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_n
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
// freshly because the async prompt outlives the caller's stack copy of it.
void persist_grant(const std::string& plugin_key, AuditEventCategory category, bool by_event, const std::vector<std::string>& keys)
void persist_grant(const std::string& plugin_key, AuditEventCategory category, const std::vector<std::string>& targets)
{
PluginInstallState state;
if (!PluginManager::instance().get_install_state(plugin_key, state))
return;
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
std::vector<std::string>* permission_list = permission_list_for(category, state.permissions);
if (!permission_list)
return;
for (const auto& key : keys)
persist_permission(plugin_key, state, *permission_list, key);
for (const auto& target : targets)
persist_permission(plugin_key, state, *permission_list, target);
}
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
static const char* audit_category_name(AuditEventCategory category)
{
switch (category) {
case AuditEventCategory::FsRead: return "FsRead";
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
case AuditEventCategory::FsCreate: return "FsCreate";
case AuditEventCategory::FsDelete: return "FsDelete";
case AuditEventCategory::Http: return "Http";
case AuditEventCategory::Socket: return "Socket";
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
case AuditEventCategory::Threading: return "Threading";
default: return "None";
}
}
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
// no target (by_event), which the prompt does not show as a target.
// Requests permission for an audited event, returning true when it is already granted or the user
// approves an inline prompt.
//
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
@@ -1051,227 +929,56 @@ static const char* audit_category_name(AuditEventCategory category)
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
// re-prompting.
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
const std::string& event_name, const std::vector<std::string>& unresolved)
{
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
std::string targets_text = by_event ? "<none>" : "";
for (const auto& target : shown) {
if (!targets_text.empty())
targets_text += ", ";
targets_text += target;
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT PROMPT] plugin=" << plugin_key << " event=" << event_name
<< " category=" << audit_category_name(category) << " targets=[" << targets_text << "]"
<< (by_event && !keys.empty() ? " grant=" + keys.front() : std::string{});
if (s_permission_prompt_for_testing)
return s_permission_prompt_for_testing(event_name, shown);
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
if (wxIsMainThread())
return prompt_for_targets(category, plugin_name, event_name, shown);
return prompt_for_targets(category, plugin_name, event_name, unresolved);
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, by_event, keys, shown]() {
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, unresolved]() {
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return;
if (prompt_for_targets(category, plugin_name, event_name, shown))
persist_grant(plugin_key, category, by_event, keys);
if (prompt_for_targets(category, plugin_name, event_name, unresolved))
persist_grant(plugin_key, category, unresolved);
});
return false;
}
// Decides an event that no allowed root or running operation covers: a saved grant allows it,
// otherwise the user is asked and a Yes is saved.
int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state,
const std::string& plugin_key,
const std::string& plugin_name,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets)
const std::vector<std::string>& targets,
std::vector<std::string>* permission_list,
const std::vector<std::string>& call_site_ids)
{
const bool by_event = targets.empty();
const std::vector<std::string> keys = by_event ? std::vector<std::string>{event_grant_key(category, event_name)} : targets;
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
std::vector<std::string> unresolved = keys;
std::vector<std::string> unresolved = targets;
if (permission_list) {
unresolved.clear();
for (const auto& key : keys)
if (!has_permission(*permission_list, key))
unresolved.push_back(key);
if (unresolved.empty())
for (const auto& target : targets)
if (!has_permission(*permission_list, target))
unresolved.push_back(target);
if (!targets.empty() && unresolved.empty())
return 0;
}
// Letting a root frame go runs finalizers inside a scope destructor or another event's hook,
// where no dialog may open: what they do must already be granted.
if (mgr.releasing_frames())
return report_denied(mgr, event_name, {false, "audit permission required"});
if (!request_permission(category, plugin_key, plugin_name, event_name, by_event, unresolved))
if (!request_permission(category, plugin_key, plugin_name, event_name, unresolved))
return report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list)
for (const auto& key : unresolved)
persist_permission(plugin_key, state, *permission_list, key);
for (const auto& target : unresolved)
persist_permission(plugin_key, state, *permission_list, target);
mgr.record_approved_call_sites(plugin_key, call_site_ids);
return 0;
}
// Judges an audited event raised while a plugin context is active.
int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, AuditEventCategory event_type, PyObject* args)
{
const bool fs_category = is_fs_category(event_type);
const std::vector<std::string> targets = audit_targets(event_name, event_type, args);
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
// unconditional block: checked before the running-operation check below, so no granted
// operation can launder access to one, and before the allowed-root shortcut further down,
// so an allowed root (e.g. the read-only resources folder) cannot make a denied path
// underneath it reachable.
if (fs_category) {
for (const auto& target : targets) {
if (mgr.is_denied_path(boost::filesystem::path(target)))
return report_denied(mgr, event_name, {false, "denied path"}, target);
}
}
// A nested event of an operation already granted, e.g. socket.connect inside an approved
// urlopen(), is not asked again. urllib.Request is always judged on its own URL, so a
// redirect or a nested urlopen() inside a running request is not covered by it.
if (event_name != "urllib.Request" && mgr.covered_by_root_frame())
return 0;
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
// installed/system profile cache), the read-only bundled resources folder, or a per-call
// scoped root such as the current G-code folder -- is part of the plugin system's normal
// workflow and does not need a prompt.
if (fs_category && !targets.empty()) {
const bool is_write = event_type != AuditEventCategory::FsRead;
const bool all_inside_allowed_root =
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
return mgr.check_path_access(boost::filesystem::path(target), is_write).allowed;
});
if (all_inside_allowed_root)
return 0;
}
PluginInstallState state;
const bool have_install_state = PluginManager::instance().get_install_state(mgr.current_plugin(), state);
if (!have_install_state) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr.current_plugin();
}
const std::string plugin_name = state.plugin_name.empty() ? mgr.current_plugin() : state.plugin_name;
const int result = decide_audited_event(mgr, state, mgr.current_plugin(), plugin_name, event_name, event_type, targets);
// A granted operation covers its nested events while it runs. A targetless grant names no
// operation, so it covers nothing else.
if (result == 0 && !targets.empty() && operation_root_events.count(event_name))
mgr.record_root_frame();
return result;
}
} // namespace PluginAuditDetail
void PluginAuditManager::record_root_frame()
{
PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
if (!frame || !PluginAuditDetail::is_stdlib_frame(frame))
return;
if (std::find(m_root_frames.begin(), m_root_frames.end(), frame) != m_root_frames.end())
return; // glob.glob and glob.glob/2 share a frame
Py_INCREF(frame);
m_root_frames.push_back(frame);
}
bool PluginAuditManager::covered_by_root_frame() const
{
if (m_root_frames.empty())
return false;
// Only stdlib frames may sit between the event and the operation: plugin or library code
// the operation calls back into (an os.walk onerror, a urllib handler) acts on its own.
for (PyObject* frame : PluginAuditDetail::stack_frames()) {
if (std::find(m_root_frames.begin(), m_root_frames.end(), frame) != m_root_frames.end())
return true;
if (!PluginAuditDetail::is_stdlib_frame(frame))
return false;
}
return false;
}
bool PluginAuditManager::releasing_frames() const { return m_releasing_frames > 0; }
bool PluginAuditManager::is_stdlib_file(const std::string& filename)
{
// Frozen stdlib modules (os, posixpath, codecs, ...) carry a pseudo filename.
if (boost::algorithm::starts_with(filename, "<frozen "))
return true;
if (filename.empty() || filename.front() == '<')
return false;
// Both sides are resolved: co_filename follows the Python home as it was given, which may
// be a symlink.
const auto [it, inserted] = PluginAuditDetail::s_stdlib_file_verdicts.try_emplace(filename, false);
if (inserted) {
const boost::filesystem::path file(filename);
const auto inside_any = [&file](const std::vector<boost::filesystem::path>& dirs) {
return std::any_of(dirs.begin(), dirs.end(),
[&file](const boost::filesystem::path& dir) { return is_inside_allowed_root(file, dir); });
};
it->second = inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
}
return it->second;
}
bool PluginAuditManager::is_stdlib_path(const boost::filesystem::path& path)
{
if (path.empty())
return false;
const auto inside_any = [&path](const std::vector<boost::filesystem::path>& dirs) {
return std::any_of(dirs.begin(), dirs.end(),
[&path](const boost::filesystem::path& dir) { return is_inside_allowed_root(path, dir); });
};
return inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
}
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
{
if (frames.empty())
return;
// A finalizer's own denial must neither replace the PermissionError of the event being
// decided nor leave a stale denial behind for the next call.
PyObject* const error = PyErr_GetRaisedException();
const bool denial_pending = m_audit_denial_pending;
const bool has_violation = m_has_last_violation;
const AuditViolation violation = m_last_violation;
std::vector<PyObject*> released;
released.swap(frames);
++m_releasing_frames;
for (PyObject* frame : released)
Py_DECREF(frame);
--m_releasing_frames;
m_audit_denial_pending = denial_pending;
m_has_last_violation = has_violation;
m_last_violation = violation;
PyErr_SetRaisedException(error);
}
void PluginAuditManager::set_permission_prompt_for_testing(PermissionPrompt prompt)
{
PluginAuditDetail::s_permission_prompt_for_testing = std::move(prompt);
}
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
{
auto* mgr = static_cast<PluginAuditManager*>(user_data);
@@ -1299,20 +1006,62 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
if (event_type == AuditEventCategory::None)
return 0;
// Root frames whose operation has returned are let go once this event is decided, so that a
// finalizer they run re-enters a hook whose held frames are already consistent.
std::vector<PyObject*> returned_frames;
PluginAuditDetail::take_returned_frames(m_root_frames, returned_frames);
const int result = PluginAuditDetail::audit_plugin_event(*mgr, event_name, event_type, args);
mgr->release_frames(returned_frames);
return result;
const bool fs_category = is_fs_category(event_type);
const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args);
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
// unconditional block: checked before the ancestor-cascade check below, so a cascade
// approval recorded for an unrelated action can never launder access to one, and before
// the allowed-root shortcut further down, so an allowed root (e.g. the read-only resources
// folder) cannot make a denied path underneath it reachable.
if (fs_category) {
for (const auto& target : targets) {
if (mgr->is_denied_path(boost::filesystem::path(target)))
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"}, target);
}
}
PluginDescriptor plugin_descriptor;
PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor);
// Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request
// was already approved, socket.connect won't trigger another permission dialog.
const std::vector<std::string> call_site_ids =
PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root);
if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids))
return 0;
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
// installed/system profile cache), the read-only bundled resources folder, or a per-call
// scoped root such as the current G-code folder -- is part of the plugin system's normal
// workflow and does not need a prompt.
if (fs_category && !targets.empty()) {
const bool is_write = event_type != AuditEventCategory::FsRead;
const bool all_inside_allowed_root =
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
return mgr->check_path_access(boost::filesystem::path(target), is_write).allowed;
});
if (all_inside_allowed_root)
return 0;
}
PluginInstallState state;
const bool have_install_state = PluginManager::instance().get_install_state(mgr->current_plugin(), state);
if (!have_install_state) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr->current_plugin();
}
const std::string plugin_name = state.plugin_name.empty() ? mgr->current_plugin() : state.plugin_name;
std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions);
return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name,
event_type, targets, permission_list, call_site_ids);
}
void PluginAuditManager::install_hook()
{
// Identifies stdlib frames for operation roots and stdlib paths for read-only access.
PluginAuditDetail::collect_stdlib_dirs();
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
// covers the plugin system's own workflow needs -- each plugin's storage folder
// (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) --
+13 -39
View File
@@ -4,9 +4,10 @@
// Via pybind11 so this file requests the same python3xx.lib as everything else.
#include <boost/filesystem/path.hpp>
#include <pybind11/conduit/wrap_include_python_h.h>
#include <functional>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <boost/filesystem.hpp>
@@ -143,26 +144,15 @@ public:
void clear_last_violation();
bool last_violation(AuditViolation& violation) const;
// --- operation-root frames ---
// One stdlib operation fires several nested audit events as it passes through lower layers:
// urlopen() raises urllib.Request, then http.client and socket events. When an operation's
// own root event is granted (by a saved grant or the user), the stdlib frame that raised it
// is held, and a later event whose stack reaches that frame through stdlib frames only is
// part of the same operation, so it is allowed without a prompt of its own. Plugin or
// library code the operation calls back into is judged on its own. A frame is let go once it
// returns, a suspended generator's (os.walk) when the audit scope ends. Letting a frame go may
// run finalizers of objects its locals held; they are judged as the plugin and never prompt.
void record_root_frame();
bool covered_by_root_frame() const;
bool releasing_frames() const;
// True when code from `filename` (a code object's co_filename) is the interpreter's own
// stdlib: the only code a root frame's coverage passes through. Valid after install_hook().
static bool is_stdlib_file(const std::string& filename);
// Answers permission prompts in place of the dialog, for tests. Production never sets it.
using PermissionPrompt = std::function<bool(const std::string& event_name, const std::vector<std::string>& targets)>;
static void set_permission_prompt_for_testing(PermissionPrompt prompt);
// --- call-site cascade cache ---
// A single plugin action often fires several nested CPython audit events as it passes
// through stdlib layers (urllib.request calling http.client calling socket, for example).
// Once the user approves one event, every stdlib frame still on the stack for that call
// is recorded here by (filename, function, first line) identity. A later event whose own
// ancestor chain still contains one of those frames is the same logical action seen from
// a deeper layer, so it is auto-approved instead of prompting again.
bool has_approved_ancestor(const std::string& plugin_key, const std::vector<std::string>& call_site_ids) const;
void record_approved_call_sites(const std::string& plugin_key, const std::vector<std::string>& call_site_ids);
bool verbose_events = true;
@@ -173,37 +163,24 @@ private:
static int audit_hook(const char* event, PyObject* args, void* user_data);
// True for paths inside the interpreter's stdlib, excluding site-packages. Unlike
// is_stdlib_file(), this query is not cached because filesystem paths may resolve through
// symlinks that can change.
static bool is_stdlib_path(const boost::filesystem::path& path);
// Drops the references in `frames`, keeping the caller's error and denial state intact.
void release_frames(std::vector<PyObject*>& frames);
static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name;
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
static thread_local std::vector<PyObject*> m_root_frames; // strong references
static thread_local int m_releasing_frames;
static thread_local bool m_audit_denial_pending;
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
// mutable: is_denied_filename() is a const query that must lock.
// mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock.
mutable std::mutex m_mutex;
std::vector<AllowedRoot> m_global_allowed_roots;
std::vector<std::string> m_denied_filenames;
std::vector<std::string> m_denied_path_keywords;
std::unordered_map<std::string, std::unordered_set<std::string>> m_approved_call_sites; // plugin_key -> call-site ids
};
// RAII guard that sets the current plugin key and capability name, restoring the previous
// pair on scope exit. `capability_name` may be empty for calls that are not scoped to a
// single capability.
//
// Each scope owns the root frames recorded inside it and lets them go, as its own plugin, before
// restoring the outer context. That needs the GIL whenever a frame is still held, so construct
// the scope after the GIL guard, to be destroyed before it.
class ScopedPluginAuditContext
{
public:
@@ -217,12 +194,9 @@ public:
ScopedPluginAuditContext& operator=(const ScopedPluginAuditContext&) = delete;
private:
std::string m_plugin_key;
std::string m_capability;
std::string m_previous_id;
std::string m_previous_capability;
std::vector<AllowedRoot> m_previous_scoped_roots;
std::vector<PyObject*> m_previous_root_frames;
};
} // namespace Slic3r
-2
View File
@@ -808,7 +808,6 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
read_string_list("network_socket", parsed.permissions.network_socket);
read_string_list("process", parsed.permissions.process);
read_string_list("threading", parsed.permissions.threading);
read_string_list("events", parsed.permissions.events);
}
if (state.contains("enabled") && state["enabled"].is_boolean())
@@ -853,7 +852,6 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
{"network_socket", state.permissions.network_socket},
{"process", state.permissions.process},
{"threading", state.permissions.threading},
{"events", state.permissions.events},
};
nlohmann::json capabilities = nlohmann::json::array();
-2
View File
@@ -104,8 +104,6 @@ struct PluginPermissions
std::vector<std::string> network_socket;
std::vector<std::string> process;
std::vector<std::string> threading;
// Grants for audit events that expose no target, keyed "<category>:<event>" (e.g. "Socket:socket.__new__").
std::vector<std::string> events;
};
struct PluginInstallState {
+1 -1
View File
@@ -102,9 +102,9 @@ public:
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
nlohmann::json get_default_config() const override
{
pybind11::gil_scoped_acquire gil;
ORCA_PY_AUDIT_SCOPE();
try {
pybind11::gil_scoped_acquire gil;
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
if (!override)
return Base::get_default_config();
+6
View File
@@ -2,6 +2,7 @@
#include "GeneratedConfig.hpp"
#include "libslic3r/Utils.hpp"
#include "PluginAuditManager.hpp"
#include "PythonPluginBridge.hpp"
#include <atomic>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
@@ -664,6 +665,11 @@ bool PythonInterpreter::initialize()
// background-thread exceptions) to <data_dir>/log/python_*.log.
install_python_stderr_redirect();
// Attribute plugin-spawned Python threads to the plugin/capability that created them.
// Must run while the GIL is still held here, after the audit hook is in place and
// before the GIL is released below.
install_python_thread_audit_shim();
// Release the GIL so other threads can acquire it via PyGILState_Ensure.
// Without this, calls from background threads will block trying to acquire the GIL.
m_main_thread_state = PyEval_SaveThread();
+87
View File
@@ -603,6 +603,93 @@ request aborts the plugin load.)pbdoc");
}
void install_python_thread_audit_shim()
{
// Runs with the GIL held from PythonInterpreter::initialize(). CPython's own thread bootstrap
// calls the target with no knowledge of the audit context that created it: the calling
// thread is the only place the plugin key and capability are still available. Capture them
// here, then re-establish the same ScopedPluginAuditContext inside the spawned thread before
// the plugin body runs. The identity lives only in this C++ closure -- no Python-visible
// setter is added.
try {
py::object thread_module = py::module_::import("_thread");
const int installed = PyObject_HasAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed");
if (installed == 1) {
BOOST_LOG_TRIVIAL(debug) << "[AUDIT] Python thread attribution shim already installed";
return;
}
if (installed < 0)
PyErr_Clear();
py::object original = py::reinterpret_steal<py::object>(
PyObject_GetAttrString(thread_module.ptr(), "start_new_thread"));
if (!original || !PyCallable_Check(original.ptr())) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error) << "[AUDIT] _thread.start_new_thread is missing or not callable; "
"plugin threads will run without audit attribution";
return;
}
py::object shim = py::cpp_function(
[original](py::object function, py::object args, py::object kwargs) -> py::object {
const std::string plugin_key = PluginAuditManager::instance().current_plugin();
if (plugin_key.empty()) {
// Not called from a plugin: preserve the stock implementation untouched.
return original(function, args, kwargs);
}
const std::string capability = PluginAuditManager::instance().current_capability();
py::object guarded = py::cpp_function(
[function, plugin_key, capability](py::args call_args, py::kwargs call_kwargs) -> py::object {
ScopedPluginAuditContext audit_scope(plugin_key, capability);
PyObject* result = PyObject_Call(function.ptr(), call_args.ptr(), call_kwargs.ptr());
if (result == nullptr)
throw py::error_already_set();
return py::reinterpret_steal<py::object>(result);
});
return original(guarded, args, kwargs);
},
py::arg("function"), py::arg("args"), py::arg("kwargs") = py::none());
if (PyObject_SetAttrString(thread_module.ptr(), "start_new_thread", shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error)
<< "[AUDIT] Failed to install Python thread attribution shim on _thread.start_new_thread";
return;
}
if (PyObject_SetAttrString(thread_module.ptr(), "_orca_thread_audit_shim_installed", Py_True) < 0)
PyErr_Clear();
// threading.py caches the function at import time (as `_start_new_thread` in 3.12, and as
// `start_new_thread` in older versions). Patch the cached reference too when the module is
// already loaded; otherwise its Thread.start() would keep calling the unwrapped original.
py::object sys_modules = py::module_::import("sys").attr("modules");
PyObject* threading_module = PyDict_GetItemString(sys_modules.ptr(), "threading");
if (threading_module && threading_module != Py_None) {
const char* cached_names[] = {"_start_new_thread", "start_new_thread"};
for (const char* cached_name : cached_names) {
const int has_cached_name = PyObject_HasAttrString(threading_module, cached_name);
if (has_cached_name < 0) {
PyErr_Clear();
continue;
}
if (has_cached_name != 1)
continue;
if (PyObject_SetAttrString(threading_module, cached_name, shim.ptr()) < 0) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(warning) << "[AUDIT] Failed to update threading." << cached_name
<< " with the Python thread attribution shim";
}
}
}
BOOST_LOG_TRIVIAL(info) << "[AUDIT] Python thread attribution shim installed";
} catch (const std::exception& ex) {
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to install Python thread attribution shim: " << ex.what();
}
}
} // namespace Slic3r
#ifdef ORCA_PYTHON_STUBGEN_MODULE
+9
View File
@@ -13,6 +13,15 @@ namespace Slic3r { class PluginCapabilityInterface; }
namespace Slic3r {
// Install the thread-attribution shim: wraps `_thread.start_new_thread` (and the reference
// cached by an already-imported `threading` module) so a thread spawned from inside a plugin
// call runs with that call's plugin key/capability audit scope. The captured identity is held
// in a private C++ wrapper; nothing is exposed to Python that could set it.
//
// Call with the GIL held, after PluginAuditManager::install_hook() and before the interpreter
// releases the GIL. Safe to call more than once per interpreter.
void install_python_thread_audit_shim();
// One materialized capability returned from finalize_plugin_capture: the C++ instance
// and its resolved get_name() (cached while the GIL was held).
struct CapturedCapability
@@ -282,11 +282,11 @@ public:
int request_bind_ticket(std::string* ticket) override
{
try {
ORCA_PY_AUDIT_SCOPE();
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil;
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
ORCA_PY_AUDIT_SCOPE();
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
+20
View File
@@ -0,0 +1,20 @@
`two_color_jpeg.glb` is a generated, closed tetrahedron with four faces, UVs, and
an embedded 8×8 red/blue JPEG base-color texture. Its coordinates span 20 mm.
The core tests load all three fixtures through `Model::read_from_file`, convert
textures or MTL colors to painting with boundary smoothing, and match red and blue
to filaments. They run in CI without a display. A separate layout check compares
the smoothing search-tree type seen by a core consumer with the same type compiled
inside `libslic3r_cgal`. The mismatch is checked directly because undefined behavior
can be masked by which inline template implementations the linker selects.
`two_color_sphere/test_two_color_sphere.obj` and its matching MTL are the original
red/blue material-color sphere supplied by RF47 in
[the PR discussion](https://github.com/OrcaSlicer/OrcaSlicer/pull/16607#issuecomment-6244475095).
They contain solid `Kd` colors and no texture images.
`test_two_color_sphere_textured.obj` is a derived version of that sphere. Its MTL
uses white `Kd` values and references `textures/two_color.jpg` by a relative path;
the two face groups sample different halves of that image through their UVs.
The JPEG is extracted unchanged from `two_color_jpeg.glb`. This case must retain
both colors from the external texture, rather than fall back to MTL colors.
Binary file not shown.
@@ -0,0 +1,11 @@
# test model for OrcaSlicer tex2color
newmtl Red
Ka 0.0000 0.0000 0.0000
Kd 0.7500 0.2500 0.2500
Ks 0.0000 0.0000 0.0000
d 1.0000
newmtl Blue
Ka 0.0000 0.0000 0.0000
Kd 0.0700 0.2100 0.7000
Ks 0.0000 0.0000 0.0000
d 1.0000
@@ -0,0 +1,457 @@
# test model for OrcaSlicer tex2color
mtllib test_two_color_sphere.mtl
v 0.000000 0.000000 20.000000
v 7.653669 0.000000 18.477591
v 7.071068 2.928932 18.477591
v 5.411961 5.411961 18.477591
v 2.928932 7.071068 18.477591
v 0.000000 7.653669 18.477591
v -2.928932 7.071068 18.477591
v -5.411961 5.411961 18.477591
v -7.071068 2.928932 18.477591
v -7.653669 0.000000 18.477591
v -7.071068 -2.928932 18.477591
v -5.411961 -5.411961 18.477591
v -2.928932 -7.071068 18.477591
v -0.000000 -7.653669 18.477591
v 2.928932 -7.071068 18.477591
v 5.411961 -5.411961 18.477591
v 7.071068 -2.928932 18.477591
v 14.142136 0.000000 14.142136
v 13.065630 5.411961 14.142136
v 10.000000 10.000000 14.142136
v 5.411961 13.065630 14.142136
v 0.000000 14.142136 14.142136
v -5.411961 13.065630 14.142136
v -10.000000 10.000000 14.142136
v -13.065630 5.411961 14.142136
v -14.142136 0.000000 14.142136
v -13.065630 -5.411961 14.142136
v -10.000000 -10.000000 14.142136
v -5.411961 -13.065630 14.142136
v -0.000000 -14.142136 14.142136
v 5.411961 -13.065630 14.142136
v 10.000000 -10.000000 14.142136
v 13.065630 -5.411961 14.142136
v 18.477591 0.000000 7.653669
v 17.071068 7.071068 7.653669
v 13.065630 13.065630 7.653669
v 7.071068 17.071068 7.653669
v 0.000000 18.477591 7.653669
v -7.071068 17.071068 7.653669
v -13.065630 13.065630 7.653669
v -17.071068 7.071068 7.653669
v -18.477591 0.000000 7.653669
v -17.071068 -7.071068 7.653669
v -13.065630 -13.065630 7.653669
v -7.071068 -17.071068 7.653669
v -0.000000 -18.477591 7.653669
v 7.071068 -17.071068 7.653669
v 13.065630 -13.065630 7.653669
v 17.071068 -7.071068 7.653669
v 20.000000 0.000000 0.000000
v 18.477591 7.653669 0.000000
v 14.142136 14.142136 0.000000
v 7.653669 18.477591 0.000000
v 0.000000 20.000000 0.000000
v -7.653669 18.477591 0.000000
v -14.142136 14.142136 0.000000
v -18.477591 7.653669 0.000000
v -20.000000 0.000000 0.000000
v -18.477591 -7.653669 0.000000
v -14.142136 -14.142136 0.000000
v -7.653669 -18.477591 0.000000
v -0.000000 -20.000000 0.000000
v 7.653669 -18.477591 0.000000
v 14.142136 -14.142136 0.000000
v 18.477591 -7.653669 0.000000
v 18.477591 0.000000 -7.653669
v 17.071068 7.071068 -7.653669
v 13.065630 13.065630 -7.653669
v 7.071068 17.071068 -7.653669
v 0.000000 18.477591 -7.653669
v -7.071068 17.071068 -7.653669
v -13.065630 13.065630 -7.653669
v -17.071068 7.071068 -7.653669
v -18.477591 0.000000 -7.653669
v -17.071068 -7.071068 -7.653669
v -13.065630 -13.065630 -7.653669
v -7.071068 -17.071068 -7.653669
v -0.000000 -18.477591 -7.653669
v 7.071068 -17.071068 -7.653669
v 13.065630 -13.065630 -7.653669
v 17.071068 -7.071068 -7.653669
v 14.142136 0.000000 -14.142136
v 13.065630 5.411961 -14.142136
v 10.000000 10.000000 -14.142136
v 5.411961 13.065630 -14.142136
v 0.000000 14.142136 -14.142136
v -5.411961 13.065630 -14.142136
v -10.000000 10.000000 -14.142136
v -13.065630 5.411961 -14.142136
v -14.142136 0.000000 -14.142136
v -13.065630 -5.411961 -14.142136
v -10.000000 -10.000000 -14.142136
v -5.411961 -13.065630 -14.142136
v -0.000000 -14.142136 -14.142136
v 5.411961 -13.065630 -14.142136
v 10.000000 -10.000000 -14.142136
v 13.065630 -5.411961 -14.142136
v 7.653669 0.000000 -18.477591
v 7.071068 2.928932 -18.477591
v 5.411961 5.411961 -18.477591
v 2.928932 7.071068 -18.477591
v 0.000000 7.653669 -18.477591
v -2.928932 7.071068 -18.477591
v -5.411961 5.411961 -18.477591
v -7.071068 2.928932 -18.477591
v -7.653669 0.000000 -18.477591
v -7.071068 -2.928932 -18.477591
v -5.411961 -5.411961 -18.477591
v -2.928932 -7.071068 -18.477591
v -0.000000 -7.653669 -18.477591
v 2.928932 -7.071068 -18.477591
v 5.411961 -5.411961 -18.477591
v 7.071068 -2.928932 -18.477591
v 0.000000 0.000000 -20.000000
vt 0 0
vn 0.000000 -0.000000 1.000000
vn 0.439605 0.035510 0.897489
vn 0.392553 0.201037 0.897489
vn 0.285739 0.335958 0.897489
vn 0.135423 0.419732 0.897489
vn -0.035510 0.439605 0.897489
vn -0.201037 0.392553 0.897489
vn -0.335958 0.285739 0.897489
vn -0.419732 0.135423 0.897489
vn -0.439605 -0.035510 0.897489
vn -0.392553 -0.201037 0.897489
vn -0.285739 -0.335958 0.897489
vn -0.135423 -0.419732 0.897489
vn 0.035510 -0.439605 0.897489
vn 0.201037 -0.392553 0.897489
vn 0.335958 -0.285739 0.897489
vn 0.419732 -0.135423 0.897489
vn 0.725001 0.027230 0.688209
vn 0.659393 0.302603 0.688209
vn 0.493399 0.531907 0.688209
vn 0.252289 0.680234 0.688209
vn -0.027230 0.725001 0.688209
vn -0.302603 0.659393 0.688209
vn -0.531907 0.493399 0.688209
vn -0.680234 0.252289 0.688209
vn -0.725001 -0.027230 0.688209
vn -0.659393 -0.302603 0.688209
vn -0.493399 -0.531907 0.688209
vn -0.252289 -0.680234 0.688209
vn 0.027230 -0.725001 0.688209
vn 0.302603 -0.659393 0.688209
vn 0.531907 -0.493399 0.688209
vn 0.680234 -0.252289 0.688209
vn 0.927851 0.014745 0.372658
vn 0.851580 0.368696 0.372658
vn 0.645664 0.666516 0.372658
vn 0.341451 0.862865 0.372658
vn -0.014745 0.927851 0.372658
vn -0.368696 0.851580 0.372658
vn -0.666516 0.645664 0.372658
vn -0.862865 0.341451 0.372658
vn -0.927851 -0.014745 0.372658
vn -0.851580 -0.368696 0.372658
vn -0.645664 -0.666516 0.372658
vn -0.341451 -0.862865 0.372658
vn 0.014745 -0.927851 0.372658
vn 0.368696 -0.851580 0.372658
vn 0.666516 -0.645664 0.372658
vn 0.862865 -0.341451 0.372658
vn 1.000000 -0.000000 -0.000000
vn 0.923880 0.382683 0.000000
vn 0.707107 0.707107 0.000000
vn 0.382683 0.923880 0.000000
vn 0.000000 1.000000 0.000000
vn -0.382683 0.923880 0.000000
vn -0.707107 0.707107 0.000000
vn -0.923880 0.382683 0.000000
vn -1.000000 0.000000 0.000000
vn -0.923880 -0.382683 -0.000000
vn -0.707107 -0.707107 0.000000
vn -0.382683 -0.923880 -0.000000
vn -0.000000 -1.000000 -0.000000
vn 0.382683 -0.923880 0.000000
vn 0.707107 -0.707107 0.000000
vn 0.923880 -0.382683 -0.000000
vn 0.927851 -0.014745 -0.372658
vn 0.862865 0.341451 -0.372658
vn 0.666516 0.645664 -0.372658
vn 0.368696 0.851580 -0.372658
vn 0.014745 0.927851 -0.372658
vn -0.341451 0.862865 -0.372658
vn -0.645664 0.666516 -0.372658
vn -0.851580 0.368696 -0.372658
vn -0.927851 0.014745 -0.372658
vn -0.862865 -0.341451 -0.372658
vn -0.666516 -0.645664 -0.372658
vn -0.368696 -0.851580 -0.372658
vn -0.014745 -0.927851 -0.372658
vn 0.341451 -0.862865 -0.372658
vn 0.645664 -0.666516 -0.372658
vn 0.851580 -0.368696 -0.372658
vn 0.725001 -0.027230 -0.688209
vn 0.680234 0.252289 -0.688209
vn 0.531907 0.493399 -0.688209
vn 0.302603 0.659393 -0.688209
vn 0.027230 0.725001 -0.688209
vn -0.252289 0.680234 -0.688209
vn -0.493399 0.531907 -0.688209
vn -0.659393 0.302603 -0.688209
vn -0.725001 0.027230 -0.688209
vn -0.680234 -0.252289 -0.688209
vn -0.531907 -0.493399 -0.688209
vn -0.302603 -0.659393 -0.688209
vn -0.027230 -0.725001 -0.688209
vn 0.252289 -0.680234 -0.688209
vn 0.493399 -0.531907 -0.688209
vn 0.659393 -0.302603 -0.688209
vn 0.439605 -0.035510 -0.897489
vn 0.419732 0.135423 -0.897489
vn 0.335958 0.285739 -0.897489
vn 0.201037 0.392553 -0.897489
vn 0.035510 0.439605 -0.897489
vn -0.135423 0.419732 -0.897489
vn -0.285739 0.335958 -0.897489
vn -0.392553 0.201037 -0.897489
vn -0.439605 0.035510 -0.897489
vn -0.419732 -0.135423 -0.897489
vn -0.335958 -0.285739 -0.897489
vn -0.201037 -0.392553 -0.897489
vn -0.035510 -0.439605 -0.897489
vn 0.135423 -0.419732 -0.897489
vn 0.285739 -0.335958 -0.897489
vn 0.392553 -0.201037 -0.897489
vn 0.000000 -0.000000 -1.000000
usemtl Red
f 1/1/1 2/1/2 3/1/3
f 1/1/1 3/1/3 4/1/4
f 1/1/1 4/1/4 5/1/5
f 1/1/1 5/1/5 6/1/6
f 1/1/1 6/1/6 7/1/7
f 1/1/1 7/1/7 8/1/8
f 1/1/1 8/1/8 9/1/9
f 1/1/1 9/1/9 10/1/10
f 1/1/1 10/1/10 11/1/11
f 1/1/1 11/1/11 12/1/12
f 1/1/1 12/1/12 13/1/13
f 1/1/1 13/1/13 14/1/14
f 1/1/1 14/1/14 15/1/15
f 1/1/1 15/1/15 16/1/16
f 1/1/1 16/1/16 17/1/17
f 1/1/1 17/1/17 2/1/2
f 2/1/2 18/1/18 19/1/19
f 2/1/2 19/1/19 3/1/3
f 3/1/3 19/1/19 20/1/20
f 3/1/3 20/1/20 4/1/4
f 4/1/4 20/1/20 21/1/21
f 4/1/4 21/1/21 5/1/5
f 5/1/5 21/1/21 22/1/22
f 5/1/5 22/1/22 6/1/6
f 6/1/6 22/1/22 23/1/23
f 6/1/6 23/1/23 7/1/7
f 7/1/7 23/1/23 24/1/24
f 7/1/7 24/1/24 8/1/8
f 8/1/8 24/1/24 25/1/25
f 8/1/8 25/1/25 9/1/9
f 9/1/9 25/1/25 26/1/26
f 9/1/9 26/1/26 10/1/10
f 10/1/10 26/1/26 27/1/27
f 10/1/10 27/1/27 11/1/11
f 11/1/11 27/1/27 28/1/28
f 11/1/11 28/1/28 12/1/12
f 12/1/12 28/1/28 29/1/29
f 12/1/12 29/1/29 13/1/13
f 13/1/13 29/1/29 30/1/30
f 13/1/13 30/1/30 14/1/14
f 14/1/14 30/1/30 31/1/31
f 14/1/14 31/1/31 15/1/15
f 15/1/15 31/1/31 32/1/32
f 15/1/15 32/1/32 16/1/16
f 16/1/16 32/1/32 33/1/33
f 16/1/16 33/1/33 17/1/17
f 17/1/17 33/1/33 18/1/18
f 17/1/17 18/1/18 2/1/2
f 18/1/18 34/1/34 35/1/35
f 18/1/18 35/1/35 19/1/19
f 19/1/19 35/1/35 36/1/36
f 19/1/19 36/1/36 20/1/20
f 20/1/20 36/1/36 37/1/37
f 20/1/20 37/1/37 21/1/21
f 21/1/21 37/1/37 38/1/38
f 21/1/21 38/1/38 22/1/22
f 22/1/22 38/1/38 39/1/39
f 22/1/22 39/1/39 23/1/23
f 23/1/23 39/1/39 40/1/40
f 23/1/23 40/1/40 24/1/24
f 24/1/24 40/1/40 41/1/41
f 24/1/24 41/1/41 25/1/25
f 25/1/25 41/1/41 42/1/42
f 25/1/25 42/1/42 26/1/26
f 26/1/26 42/1/42 43/1/43
f 26/1/26 43/1/43 27/1/27
f 27/1/27 43/1/43 44/1/44
f 27/1/27 44/1/44 28/1/28
f 28/1/28 44/1/44 45/1/45
f 28/1/28 45/1/45 29/1/29
f 29/1/29 45/1/45 46/1/46
f 29/1/29 46/1/46 30/1/30
f 30/1/30 46/1/46 47/1/47
f 30/1/30 47/1/47 31/1/31
f 31/1/31 47/1/47 48/1/48
f 31/1/31 48/1/48 32/1/32
f 32/1/32 48/1/48 49/1/49
f 32/1/32 49/1/49 33/1/33
f 33/1/33 49/1/49 34/1/34
f 33/1/33 34/1/34 18/1/18
f 34/1/34 50/1/50 51/1/51
f 34/1/34 51/1/51 35/1/35
f 35/1/35 51/1/51 52/1/52
f 35/1/35 52/1/52 36/1/36
f 36/1/36 52/1/52 53/1/53
f 36/1/36 53/1/53 37/1/37
f 37/1/37 53/1/53 54/1/54
f 37/1/37 54/1/54 38/1/38
f 38/1/38 54/1/54 55/1/55
f 38/1/38 55/1/55 39/1/39
f 39/1/39 55/1/55 56/1/56
f 39/1/39 56/1/56 40/1/40
f 40/1/40 56/1/56 57/1/57
f 40/1/40 57/1/57 41/1/41
f 41/1/41 57/1/57 58/1/58
f 41/1/41 58/1/58 42/1/42
f 42/1/42 58/1/58 59/1/59
f 42/1/42 59/1/59 43/1/43
f 43/1/43 59/1/59 60/1/60
f 43/1/43 60/1/60 44/1/44
f 44/1/44 60/1/60 61/1/61
f 44/1/44 61/1/61 45/1/45
f 45/1/45 61/1/61 62/1/62
f 45/1/45 62/1/62 46/1/46
f 46/1/46 62/1/62 63/1/63
f 46/1/46 63/1/63 47/1/47
f 47/1/47 63/1/63 64/1/64
f 47/1/47 64/1/64 48/1/48
f 48/1/48 64/1/64 65/1/65
f 48/1/48 65/1/65 49/1/49
f 49/1/49 65/1/65 50/1/50
f 49/1/49 50/1/50 34/1/34
usemtl Blue
f 50/1/50 66/1/66 67/1/67
f 50/1/50 67/1/67 51/1/51
f 51/1/51 67/1/67 68/1/68
f 51/1/51 68/1/68 52/1/52
f 52/1/52 68/1/68 69/1/69
f 52/1/52 69/1/69 53/1/53
f 53/1/53 69/1/69 70/1/70
f 53/1/53 70/1/70 54/1/54
f 54/1/54 70/1/70 71/1/71
f 54/1/54 71/1/71 55/1/55
f 55/1/55 71/1/71 72/1/72
f 55/1/55 72/1/72 56/1/56
f 56/1/56 72/1/72 73/1/73
f 56/1/56 73/1/73 57/1/57
f 57/1/57 73/1/73 74/1/74
f 57/1/57 74/1/74 58/1/58
f 58/1/58 74/1/74 75/1/75
f 58/1/58 75/1/75 59/1/59
f 59/1/59 75/1/75 76/1/76
f 59/1/59 76/1/76 60/1/60
f 60/1/60 76/1/76 77/1/77
f 60/1/60 77/1/77 61/1/61
f 61/1/61 77/1/77 78/1/78
f 61/1/61 78/1/78 62/1/62
f 62/1/62 78/1/78 79/1/79
f 62/1/62 79/1/79 63/1/63
f 63/1/63 79/1/79 80/1/80
f 63/1/63 80/1/80 64/1/64
f 64/1/64 80/1/80 81/1/81
f 64/1/64 81/1/81 65/1/65
f 65/1/65 81/1/81 66/1/66
f 65/1/65 66/1/66 50/1/50
f 66/1/66 82/1/82 83/1/83
f 66/1/66 83/1/83 67/1/67
f 67/1/67 83/1/83 84/1/84
f 67/1/67 84/1/84 68/1/68
f 68/1/68 84/1/84 85/1/85
f 68/1/68 85/1/85 69/1/69
f 69/1/69 85/1/85 86/1/86
f 69/1/69 86/1/86 70/1/70
f 70/1/70 86/1/86 87/1/87
f 70/1/70 87/1/87 71/1/71
f 71/1/71 87/1/87 88/1/88
f 71/1/71 88/1/88 72/1/72
f 72/1/72 88/1/88 89/1/89
f 72/1/72 89/1/89 73/1/73
f 73/1/73 89/1/89 90/1/90
f 73/1/73 90/1/90 74/1/74
f 74/1/74 90/1/90 91/1/91
f 74/1/74 91/1/91 75/1/75
f 75/1/75 91/1/91 92/1/92
f 75/1/75 92/1/92 76/1/76
f 76/1/76 92/1/92 93/1/93
f 76/1/76 93/1/93 77/1/77
f 77/1/77 93/1/93 94/1/94
f 77/1/77 94/1/94 78/1/78
f 78/1/78 94/1/94 95/1/95
f 78/1/78 95/1/95 79/1/79
f 79/1/79 95/1/95 96/1/96
f 79/1/79 96/1/96 80/1/80
f 80/1/80 96/1/96 97/1/97
f 80/1/80 97/1/97 81/1/81
f 81/1/81 97/1/97 82/1/82
f 81/1/81 82/1/82 66/1/66
f 82/1/82 98/1/98 99/1/99
f 82/1/82 99/1/99 83/1/83
f 83/1/83 99/1/99 100/1/100
f 83/1/83 100/1/100 84/1/84
f 84/1/84 100/1/100 101/1/101
f 84/1/84 101/1/101 85/1/85
f 85/1/85 101/1/101 102/1/102
f 85/1/85 102/1/102 86/1/86
f 86/1/86 102/1/102 103/1/103
f 86/1/86 103/1/103 87/1/87
f 87/1/87 103/1/103 104/1/104
f 87/1/87 104/1/104 88/1/88
f 88/1/88 104/1/104 105/1/105
f 88/1/88 105/1/105 89/1/89
f 89/1/89 105/1/105 106/1/106
f 89/1/89 106/1/106 90/1/90
f 90/1/90 106/1/106 107/1/107
f 90/1/90 107/1/107 91/1/91
f 91/1/91 107/1/107 108/1/108
f 91/1/91 108/1/108 92/1/92
f 92/1/92 108/1/108 109/1/109
f 92/1/92 109/1/109 93/1/93
f 93/1/93 109/1/109 110/1/110
f 93/1/93 110/1/110 94/1/94
f 94/1/94 110/1/110 111/1/111
f 94/1/94 111/1/111 95/1/95
f 95/1/95 111/1/111 112/1/112
f 95/1/95 112/1/112 96/1/96
f 96/1/96 112/1/112 113/1/113
f 96/1/96 113/1/113 97/1/97
f 97/1/97 113/1/113 98/1/98
f 97/1/97 98/1/98 82/1/82
f 114/1/114 99/1/99 98/1/98
f 114/1/114 100/1/100 99/1/99
f 114/1/114 101/1/101 100/1/100
f 114/1/114 102/1/102 101/1/101
f 114/1/114 103/1/103 102/1/102
f 114/1/114 104/1/104 103/1/103
f 114/1/114 105/1/105 104/1/104
f 114/1/114 106/1/106 105/1/105
f 114/1/114 107/1/107 106/1/106
f 114/1/114 108/1/108 107/1/107
f 114/1/114 109/1/109 108/1/108
f 114/1/114 110/1/110 109/1/109
f 114/1/114 111/1/111 110/1/110
f 114/1/114 112/1/112 111/1/111
f 114/1/114 113/1/113 112/1/112
f 114/1/114 98/1/98 113/1/113
@@ -0,0 +1,8 @@
# Derived texture-file regression: color comes from map_Kd, not Kd.
newmtl Red
Kd 1 1 1
map_Kd textures/two_color.jpg
newmtl Blue
Kd 1 1 1
map_Kd textures/two_color.jpg
@@ -0,0 +1,458 @@
# test model for OrcaSlicer tex2color
mtllib test_two_color_sphere_textured.mtl
v 0.000000 0.000000 20.000000
v 7.653669 0.000000 18.477591
v 7.071068 2.928932 18.477591
v 5.411961 5.411961 18.477591
v 2.928932 7.071068 18.477591
v 0.000000 7.653669 18.477591
v -2.928932 7.071068 18.477591
v -5.411961 5.411961 18.477591
v -7.071068 2.928932 18.477591
v -7.653669 0.000000 18.477591
v -7.071068 -2.928932 18.477591
v -5.411961 -5.411961 18.477591
v -2.928932 -7.071068 18.477591
v -0.000000 -7.653669 18.477591
v 2.928932 -7.071068 18.477591
v 5.411961 -5.411961 18.477591
v 7.071068 -2.928932 18.477591
v 14.142136 0.000000 14.142136
v 13.065630 5.411961 14.142136
v 10.000000 10.000000 14.142136
v 5.411961 13.065630 14.142136
v 0.000000 14.142136 14.142136
v -5.411961 13.065630 14.142136
v -10.000000 10.000000 14.142136
v -13.065630 5.411961 14.142136
v -14.142136 0.000000 14.142136
v -13.065630 -5.411961 14.142136
v -10.000000 -10.000000 14.142136
v -5.411961 -13.065630 14.142136
v -0.000000 -14.142136 14.142136
v 5.411961 -13.065630 14.142136
v 10.000000 -10.000000 14.142136
v 13.065630 -5.411961 14.142136
v 18.477591 0.000000 7.653669
v 17.071068 7.071068 7.653669
v 13.065630 13.065630 7.653669
v 7.071068 17.071068 7.653669
v 0.000000 18.477591 7.653669
v -7.071068 17.071068 7.653669
v -13.065630 13.065630 7.653669
v -17.071068 7.071068 7.653669
v -18.477591 0.000000 7.653669
v -17.071068 -7.071068 7.653669
v -13.065630 -13.065630 7.653669
v -7.071068 -17.071068 7.653669
v -0.000000 -18.477591 7.653669
v 7.071068 -17.071068 7.653669
v 13.065630 -13.065630 7.653669
v 17.071068 -7.071068 7.653669
v 20.000000 0.000000 0.000000
v 18.477591 7.653669 0.000000
v 14.142136 14.142136 0.000000
v 7.653669 18.477591 0.000000
v 0.000000 20.000000 0.000000
v -7.653669 18.477591 0.000000
v -14.142136 14.142136 0.000000
v -18.477591 7.653669 0.000000
v -20.000000 0.000000 0.000000
v -18.477591 -7.653669 0.000000
v -14.142136 -14.142136 0.000000
v -7.653669 -18.477591 0.000000
v -0.000000 -20.000000 0.000000
v 7.653669 -18.477591 0.000000
v 14.142136 -14.142136 0.000000
v 18.477591 -7.653669 0.000000
v 18.477591 0.000000 -7.653669
v 17.071068 7.071068 -7.653669
v 13.065630 13.065630 -7.653669
v 7.071068 17.071068 -7.653669
v 0.000000 18.477591 -7.653669
v -7.071068 17.071068 -7.653669
v -13.065630 13.065630 -7.653669
v -17.071068 7.071068 -7.653669
v -18.477591 0.000000 -7.653669
v -17.071068 -7.071068 -7.653669
v -13.065630 -13.065630 -7.653669
v -7.071068 -17.071068 -7.653669
v -0.000000 -18.477591 -7.653669
v 7.071068 -17.071068 -7.653669
v 13.065630 -13.065630 -7.653669
v 17.071068 -7.071068 -7.653669
v 14.142136 0.000000 -14.142136
v 13.065630 5.411961 -14.142136
v 10.000000 10.000000 -14.142136
v 5.411961 13.065630 -14.142136
v 0.000000 14.142136 -14.142136
v -5.411961 13.065630 -14.142136
v -10.000000 10.000000 -14.142136
v -13.065630 5.411961 -14.142136
v -14.142136 0.000000 -14.142136
v -13.065630 -5.411961 -14.142136
v -10.000000 -10.000000 -14.142136
v -5.411961 -13.065630 -14.142136
v -0.000000 -14.142136 -14.142136
v 5.411961 -13.065630 -14.142136
v 10.000000 -10.000000 -14.142136
v 13.065630 -5.411961 -14.142136
v 7.653669 0.000000 -18.477591
v 7.071068 2.928932 -18.477591
v 5.411961 5.411961 -18.477591
v 2.928932 7.071068 -18.477591
v 0.000000 7.653669 -18.477591
v -2.928932 7.071068 -18.477591
v -5.411961 5.411961 -18.477591
v -7.071068 2.928932 -18.477591
v -7.653669 0.000000 -18.477591
v -7.071068 -2.928932 -18.477591
v -5.411961 -5.411961 -18.477591
v -2.928932 -7.071068 -18.477591
v -0.000000 -7.653669 -18.477591
v 2.928932 -7.071068 -18.477591
v 5.411961 -5.411961 -18.477591
v 7.071068 -2.928932 -18.477591
v 0.000000 0.000000 -20.000000
vt 0.25 0.5
vt 0.75 0.5
vn 0.000000 -0.000000 1.000000
vn 0.439605 0.035510 0.897489
vn 0.392553 0.201037 0.897489
vn 0.285739 0.335958 0.897489
vn 0.135423 0.419732 0.897489
vn -0.035510 0.439605 0.897489
vn -0.201037 0.392553 0.897489
vn -0.335958 0.285739 0.897489
vn -0.419732 0.135423 0.897489
vn -0.439605 -0.035510 0.897489
vn -0.392553 -0.201037 0.897489
vn -0.285739 -0.335958 0.897489
vn -0.135423 -0.419732 0.897489
vn 0.035510 -0.439605 0.897489
vn 0.201037 -0.392553 0.897489
vn 0.335958 -0.285739 0.897489
vn 0.419732 -0.135423 0.897489
vn 0.725001 0.027230 0.688209
vn 0.659393 0.302603 0.688209
vn 0.493399 0.531907 0.688209
vn 0.252289 0.680234 0.688209
vn -0.027230 0.725001 0.688209
vn -0.302603 0.659393 0.688209
vn -0.531907 0.493399 0.688209
vn -0.680234 0.252289 0.688209
vn -0.725001 -0.027230 0.688209
vn -0.659393 -0.302603 0.688209
vn -0.493399 -0.531907 0.688209
vn -0.252289 -0.680234 0.688209
vn 0.027230 -0.725001 0.688209
vn 0.302603 -0.659393 0.688209
vn 0.531907 -0.493399 0.688209
vn 0.680234 -0.252289 0.688209
vn 0.927851 0.014745 0.372658
vn 0.851580 0.368696 0.372658
vn 0.645664 0.666516 0.372658
vn 0.341451 0.862865 0.372658
vn -0.014745 0.927851 0.372658
vn -0.368696 0.851580 0.372658
vn -0.666516 0.645664 0.372658
vn -0.862865 0.341451 0.372658
vn -0.927851 -0.014745 0.372658
vn -0.851580 -0.368696 0.372658
vn -0.645664 -0.666516 0.372658
vn -0.341451 -0.862865 0.372658
vn 0.014745 -0.927851 0.372658
vn 0.368696 -0.851580 0.372658
vn 0.666516 -0.645664 0.372658
vn 0.862865 -0.341451 0.372658
vn 1.000000 -0.000000 -0.000000
vn 0.923880 0.382683 0.000000
vn 0.707107 0.707107 0.000000
vn 0.382683 0.923880 0.000000
vn 0.000000 1.000000 0.000000
vn -0.382683 0.923880 0.000000
vn -0.707107 0.707107 0.000000
vn -0.923880 0.382683 0.000000
vn -1.000000 0.000000 0.000000
vn -0.923880 -0.382683 -0.000000
vn -0.707107 -0.707107 0.000000
vn -0.382683 -0.923880 -0.000000
vn -0.000000 -1.000000 -0.000000
vn 0.382683 -0.923880 0.000000
vn 0.707107 -0.707107 0.000000
vn 0.923880 -0.382683 -0.000000
vn 0.927851 -0.014745 -0.372658
vn 0.862865 0.341451 -0.372658
vn 0.666516 0.645664 -0.372658
vn 0.368696 0.851580 -0.372658
vn 0.014745 0.927851 -0.372658
vn -0.341451 0.862865 -0.372658
vn -0.645664 0.666516 -0.372658
vn -0.851580 0.368696 -0.372658
vn -0.927851 0.014745 -0.372658
vn -0.862865 -0.341451 -0.372658
vn -0.666516 -0.645664 -0.372658
vn -0.368696 -0.851580 -0.372658
vn -0.014745 -0.927851 -0.372658
vn 0.341451 -0.862865 -0.372658
vn 0.645664 -0.666516 -0.372658
vn 0.851580 -0.368696 -0.372658
vn 0.725001 -0.027230 -0.688209
vn 0.680234 0.252289 -0.688209
vn 0.531907 0.493399 -0.688209
vn 0.302603 0.659393 -0.688209
vn 0.027230 0.725001 -0.688209
vn -0.252289 0.680234 -0.688209
vn -0.493399 0.531907 -0.688209
vn -0.659393 0.302603 -0.688209
vn -0.725001 0.027230 -0.688209
vn -0.680234 -0.252289 -0.688209
vn -0.531907 -0.493399 -0.688209
vn -0.302603 -0.659393 -0.688209
vn -0.027230 -0.725001 -0.688209
vn 0.252289 -0.680234 -0.688209
vn 0.493399 -0.531907 -0.688209
vn 0.659393 -0.302603 -0.688209
vn 0.439605 -0.035510 -0.897489
vn 0.419732 0.135423 -0.897489
vn 0.335958 0.285739 -0.897489
vn 0.201037 0.392553 -0.897489
vn 0.035510 0.439605 -0.897489
vn -0.135423 0.419732 -0.897489
vn -0.285739 0.335958 -0.897489
vn -0.392553 0.201037 -0.897489
vn -0.439605 0.035510 -0.897489
vn -0.419732 -0.135423 -0.897489
vn -0.335958 -0.285739 -0.897489
vn -0.201037 -0.392553 -0.897489
vn -0.035510 -0.439605 -0.897489
vn 0.135423 -0.419732 -0.897489
vn 0.285739 -0.335958 -0.897489
vn 0.392553 -0.201037 -0.897489
vn 0.000000 -0.000000 -1.000000
usemtl Red
f 1/1/1 2/1/2 3/1/3
f 1/1/1 3/1/3 4/1/4
f 1/1/1 4/1/4 5/1/5
f 1/1/1 5/1/5 6/1/6
f 1/1/1 6/1/6 7/1/7
f 1/1/1 7/1/7 8/1/8
f 1/1/1 8/1/8 9/1/9
f 1/1/1 9/1/9 10/1/10
f 1/1/1 10/1/10 11/1/11
f 1/1/1 11/1/11 12/1/12
f 1/1/1 12/1/12 13/1/13
f 1/1/1 13/1/13 14/1/14
f 1/1/1 14/1/14 15/1/15
f 1/1/1 15/1/15 16/1/16
f 1/1/1 16/1/16 17/1/17
f 1/1/1 17/1/17 2/1/2
f 2/1/2 18/1/18 19/1/19
f 2/1/2 19/1/19 3/1/3
f 3/1/3 19/1/19 20/1/20
f 3/1/3 20/1/20 4/1/4
f 4/1/4 20/1/20 21/1/21
f 4/1/4 21/1/21 5/1/5
f 5/1/5 21/1/21 22/1/22
f 5/1/5 22/1/22 6/1/6
f 6/1/6 22/1/22 23/1/23
f 6/1/6 23/1/23 7/1/7
f 7/1/7 23/1/23 24/1/24
f 7/1/7 24/1/24 8/1/8
f 8/1/8 24/1/24 25/1/25
f 8/1/8 25/1/25 9/1/9
f 9/1/9 25/1/25 26/1/26
f 9/1/9 26/1/26 10/1/10
f 10/1/10 26/1/26 27/1/27
f 10/1/10 27/1/27 11/1/11
f 11/1/11 27/1/27 28/1/28
f 11/1/11 28/1/28 12/1/12
f 12/1/12 28/1/28 29/1/29
f 12/1/12 29/1/29 13/1/13
f 13/1/13 29/1/29 30/1/30
f 13/1/13 30/1/30 14/1/14
f 14/1/14 30/1/30 31/1/31
f 14/1/14 31/1/31 15/1/15
f 15/1/15 31/1/31 32/1/32
f 15/1/15 32/1/32 16/1/16
f 16/1/16 32/1/32 33/1/33
f 16/1/16 33/1/33 17/1/17
f 17/1/17 33/1/33 18/1/18
f 17/1/17 18/1/18 2/1/2
f 18/1/18 34/1/34 35/1/35
f 18/1/18 35/1/35 19/1/19
f 19/1/19 35/1/35 36/1/36
f 19/1/19 36/1/36 20/1/20
f 20/1/20 36/1/36 37/1/37
f 20/1/20 37/1/37 21/1/21
f 21/1/21 37/1/37 38/1/38
f 21/1/21 38/1/38 22/1/22
f 22/1/22 38/1/38 39/1/39
f 22/1/22 39/1/39 23/1/23
f 23/1/23 39/1/39 40/1/40
f 23/1/23 40/1/40 24/1/24
f 24/1/24 40/1/40 41/1/41
f 24/1/24 41/1/41 25/1/25
f 25/1/25 41/1/41 42/1/42
f 25/1/25 42/1/42 26/1/26
f 26/1/26 42/1/42 43/1/43
f 26/1/26 43/1/43 27/1/27
f 27/1/27 43/1/43 44/1/44
f 27/1/27 44/1/44 28/1/28
f 28/1/28 44/1/44 45/1/45
f 28/1/28 45/1/45 29/1/29
f 29/1/29 45/1/45 46/1/46
f 29/1/29 46/1/46 30/1/30
f 30/1/30 46/1/46 47/1/47
f 30/1/30 47/1/47 31/1/31
f 31/1/31 47/1/47 48/1/48
f 31/1/31 48/1/48 32/1/32
f 32/1/32 48/1/48 49/1/49
f 32/1/32 49/1/49 33/1/33
f 33/1/33 49/1/49 34/1/34
f 33/1/33 34/1/34 18/1/18
f 34/1/34 50/1/50 51/1/51
f 34/1/34 51/1/51 35/1/35
f 35/1/35 51/1/51 52/1/52
f 35/1/35 52/1/52 36/1/36
f 36/1/36 52/1/52 53/1/53
f 36/1/36 53/1/53 37/1/37
f 37/1/37 53/1/53 54/1/54
f 37/1/37 54/1/54 38/1/38
f 38/1/38 54/1/54 55/1/55
f 38/1/38 55/1/55 39/1/39
f 39/1/39 55/1/55 56/1/56
f 39/1/39 56/1/56 40/1/40
f 40/1/40 56/1/56 57/1/57
f 40/1/40 57/1/57 41/1/41
f 41/1/41 57/1/57 58/1/58
f 41/1/41 58/1/58 42/1/42
f 42/1/42 58/1/58 59/1/59
f 42/1/42 59/1/59 43/1/43
f 43/1/43 59/1/59 60/1/60
f 43/1/43 60/1/60 44/1/44
f 44/1/44 60/1/60 61/1/61
f 44/1/44 61/1/61 45/1/45
f 45/1/45 61/1/61 62/1/62
f 45/1/45 62/1/62 46/1/46
f 46/1/46 62/1/62 63/1/63
f 46/1/46 63/1/63 47/1/47
f 47/1/47 63/1/63 64/1/64
f 47/1/47 64/1/64 48/1/48
f 48/1/48 64/1/64 65/1/65
f 48/1/48 65/1/65 49/1/49
f 49/1/49 65/1/65 50/1/50
f 49/1/49 50/1/50 34/1/34
usemtl Blue
f 50/2/50 66/2/66 67/2/67
f 50/2/50 67/2/67 51/2/51
f 51/2/51 67/2/67 68/2/68
f 51/2/51 68/2/68 52/2/52
f 52/2/52 68/2/68 69/2/69
f 52/2/52 69/2/69 53/2/53
f 53/2/53 69/2/69 70/2/70
f 53/2/53 70/2/70 54/2/54
f 54/2/54 70/2/70 71/2/71
f 54/2/54 71/2/71 55/2/55
f 55/2/55 71/2/71 72/2/72
f 55/2/55 72/2/72 56/2/56
f 56/2/56 72/2/72 73/2/73
f 56/2/56 73/2/73 57/2/57
f 57/2/57 73/2/73 74/2/74
f 57/2/57 74/2/74 58/2/58
f 58/2/58 74/2/74 75/2/75
f 58/2/58 75/2/75 59/2/59
f 59/2/59 75/2/75 76/2/76
f 59/2/59 76/2/76 60/2/60
f 60/2/60 76/2/76 77/2/77
f 60/2/60 77/2/77 61/2/61
f 61/2/61 77/2/77 78/2/78
f 61/2/61 78/2/78 62/2/62
f 62/2/62 78/2/78 79/2/79
f 62/2/62 79/2/79 63/2/63
f 63/2/63 79/2/79 80/2/80
f 63/2/63 80/2/80 64/2/64
f 64/2/64 80/2/80 81/2/81
f 64/2/64 81/2/81 65/2/65
f 65/2/65 81/2/81 66/2/66
f 65/2/65 66/2/66 50/2/50
f 66/2/66 82/2/82 83/2/83
f 66/2/66 83/2/83 67/2/67
f 67/2/67 83/2/83 84/2/84
f 67/2/67 84/2/84 68/2/68
f 68/2/68 84/2/84 85/2/85
f 68/2/68 85/2/85 69/2/69
f 69/2/69 85/2/85 86/2/86
f 69/2/69 86/2/86 70/2/70
f 70/2/70 86/2/86 87/2/87
f 70/2/70 87/2/87 71/2/71
f 71/2/71 87/2/87 88/2/88
f 71/2/71 88/2/88 72/2/72
f 72/2/72 88/2/88 89/2/89
f 72/2/72 89/2/89 73/2/73
f 73/2/73 89/2/89 90/2/90
f 73/2/73 90/2/90 74/2/74
f 74/2/74 90/2/90 91/2/91
f 74/2/74 91/2/91 75/2/75
f 75/2/75 91/2/91 92/2/92
f 75/2/75 92/2/92 76/2/76
f 76/2/76 92/2/92 93/2/93
f 76/2/76 93/2/93 77/2/77
f 77/2/77 93/2/93 94/2/94
f 77/2/77 94/2/94 78/2/78
f 78/2/78 94/2/94 95/2/95
f 78/2/78 95/2/95 79/2/79
f 79/2/79 95/2/95 96/2/96
f 79/2/79 96/2/96 80/2/80
f 80/2/80 96/2/96 97/2/97
f 80/2/80 97/2/97 81/2/81
f 81/2/81 97/2/97 82/2/82
f 81/2/81 82/2/82 66/2/66
f 82/2/82 98/2/98 99/2/99
f 82/2/82 99/2/99 83/2/83
f 83/2/83 99/2/99 100/2/100
f 83/2/83 100/2/100 84/2/84
f 84/2/84 100/2/100 101/2/101
f 84/2/84 101/2/101 85/2/85
f 85/2/85 101/2/101 102/2/102
f 85/2/85 102/2/102 86/2/86
f 86/2/86 102/2/102 103/2/103
f 86/2/86 103/2/103 87/2/87
f 87/2/87 103/2/103 104/2/104
f 87/2/87 104/2/104 88/2/88
f 88/2/88 104/2/104 105/2/105
f 88/2/88 105/2/105 89/2/89
f 89/2/89 105/2/105 106/2/106
f 89/2/89 106/2/106 90/2/90
f 90/2/90 106/2/106 107/2/107
f 90/2/90 107/2/107 91/2/91
f 91/2/91 107/2/107 108/2/108
f 91/2/91 108/2/108 92/2/92
f 92/2/92 108/2/108 109/2/109
f 92/2/92 109/2/109 93/2/93
f 93/2/93 109/2/109 110/2/110
f 93/2/93 110/2/110 94/2/94
f 94/2/94 110/2/110 111/2/111
f 94/2/94 111/2/111 95/2/95
f 95/2/95 111/2/111 112/2/112
f 95/2/95 112/2/112 96/2/96
f 96/2/96 112/2/112 113/2/113
f 96/2/96 113/2/113 97/2/97
f 97/2/97 113/2/113 98/2/98
f 97/2/97 98/2/98 82/2/82
f 114/2/114 99/2/99 98/2/98
f 114/2/114 100/2/100 99/2/99
f 114/2/114 101/2/101 100/2/100
f 114/2/114 102/2/102 101/2/101
f 114/2/114 103/2/103 102/2/102
f 114/2/114 104/2/104 103/2/103
f 114/2/114 105/2/105 104/2/104
f 114/2/114 106/2/106 105/2/105
f 114/2/114 107/2/107 106/2/106
f 114/2/114 108/2/108 107/2/107
f 114/2/114 109/2/109 108/2/108
f 114/2/114 110/2/110 109/2/109
f 114/2/114 111/2/111 110/2/110
f 114/2/114 112/2/112 111/2/111
f 114/2/114 113/2/113 112/2/112
f 114/2/114 98/2/98 113/2/113
Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

+98 -34
View File
@@ -37,7 +37,7 @@
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Fill/FillGyroid.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Fill/AdaptiveInfillField.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp"
@@ -1719,9 +1719,10 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "
const SparseInfillShape smooth = shape_for("100%");
REQUIRE(sharp.path_count > 0);
// Smoothing should leave the loop count about where it was; uncapping the smoothing reach, the
// regression this guards against, adds about 10%.
const size_t allowed_extra = sharp.path_count / 50; // 2%
// Smoothing should leave the path count about where it was; uncapping the smoothing reach, the
// regression this guards against, adds a quarter to a third. Outline ends next to each other are
// joined into one path, so a few loops more or less are a larger share of the count.
const size_t allowed_extra = sharp.path_count / 10; // 10%
REQUIRE(smooth.path_count <= sharp.path_count + allowed_extra);
// The outlines are still rounded.
REQUIRE(smooth.point_count > sharp.point_count);
@@ -2060,9 +2061,9 @@ TEST_CASE("Adaptive TPMS infill thins out from the surface to the interior densi
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(60)}, print,
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2089,9 +2090,9 @@ TEST_CASE("Adaptive TPMS infill of a tall object is sparsest at its middle heigh
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"},
{"tpms_adaptive", "lobes"},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"adaptive_infill_mode", "lobes"},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
const Polygons core = centered_square(5.);
@@ -2111,9 +2112,9 @@ TEST_CASE("2D adaptive TPMS infill does not change along its axis", "[Fill]")
Slic3r::Test::init_and_process_print({along_x ? make_cube(90., 30., 30.) : make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2147,9 +2148,9 @@ TEST_CASE("Adaptive TPMS shells and blend keep the whole deep core of a tall obj
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2179,9 +2180,9 @@ TEST_CASE("Adaptive TPMS infill is sparse at the center of both of two united sp
Slic3r::Test::init_and_process_print({TriangleMesh(spheres)}, print,
{{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "25%"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2209,9 +2210,9 @@ TEST_CASE("Adaptive TPMS gradients keep the surface density deeper in the order
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(40)}, print,
{{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"},
{"tpms_adaptive", "lobes"},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", gradient},
{"adaptive_infill_mode", "lobes"},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", gradient},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
return sparse_infill_length(print, 0., 40., centered_square(20.));
@@ -2236,9 +2237,9 @@ TEST_CASE("Adaptive TPMS settings leave the infill unchanged when they do not ap
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "20%"},
{"layer_height", 0.2},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "40%"},
{"tpms_adaptive_gradient", "exponential"}});
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "40%"},
{"adaptive_infill_gradient", "exponential"}});
const SparseInfillShape expected = sparse_infill_shape(reference);
REQUIRE(expected.path_count > 0);
CHECK(sparse_infill_shape(tuned).sequence == expected.sequence);
@@ -2252,7 +2253,7 @@ TEST_CASE("Adaptive gyroid infill ignores the Z-buckling optimization", "[Fill]"
{{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "10%"},
{"gyroid_optimized", gyroid_optimized},
{"tpms_adaptive", "lobes"},
{"adaptive_infill_mode", "lobes"},
{"layer_height", 0.2}});
return sparse_infill_shape(print);
};
@@ -2268,10 +2269,10 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
// An 80 x 80 x 40 mm box around both regions, which share its radial field.
const ExPolygons box{ExPolygon(Points{Point::new_scale(60., 20.), Point::new_scale(140., 20.), Point::new_scale(140., 100.),
Point::new_scale(60., 100.)})};
std::vector<TpmsRadialField::Slice> slices;
std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 200; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &box});
const TpmsRadialField field(slices, get_extents(box), TpmsAdaptiveMode::Lobes, [] {});
const AdaptiveInfillField field(slices, get_extents(box), AdaptiveInfillMode::Lobes, [] {});
auto circle = [](double radius) {
Polygon contour = make_circle_num_segments(scale_(radius), 120);
@@ -2285,13 +2286,13 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
filler->spacing = 0.45;
filler->angle = float(M_PI / 7.);
filler->z = z;
filler->tpms_radial_field = &field;
filler->adaptive_infill_field = &field;
FillParams params;
params.density = 0.2f;
params.tpms_adaptive = TpmsAdaptiveMode::Lobes;
params.tpms_interior_density = 0.05f;
params.tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
params.adaptive_infill_mode = AdaptiveInfillMode::Lobes;
params.adaptive_infill_interior_density = 0.05f;
params.adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
params.layer_height = 0.2;
params.dont_adjust = true;
Surface surface(stInternal, expolygon);
@@ -2328,8 +2329,8 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(30)}, print,
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "25%"},
{"tpms_adaptive", "lobes"},
{"tpms_interior_density", "5%"},
{"adaptive_infill_mode", "lobes"},
{"adaptive_infill_interior_density", "5%"},
{"layer_height", 0.2},
{"initial_layer_print_height", 0.2},
{"resolution", 0.012}});
@@ -2353,3 +2354,66 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
max_distance = std::max(max_distance, printed_tree.distance_from_lines<false>(point));
CHECK(unscale<double>(max_distance) <= 0.012);
}
TEST_CASE("Infill lines whose ends are next to each other join into one path", "[Fill]")
{
// An out-and-back pair 1 mm apart, as multiline infill leaves, and a line 19 mm away.
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
Polyline(Point::new_scale(0., 20.), Point::new_scale(10., 20.)) };
join_close_ends(lines, scale_(1.5), true);
REQUIRE(lines.size() == 2);
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
REQUIRE(joined != lines.end());
// Both lines plus the 1 mm between them, joined at one end only, as joining the other end too would close a loop.
CHECK_THAT(unscale<double>(joined->length()), Catch::Matchers::WithinAbs(21., 1e-6));
CHECK(joined->first_point() != joined->last_point());
}
TEST_CASE("Joined infill lines print each line once", "[Fill]")
{
// The middle line comes first and both of its ends get joined, so the group has to be walked from an outer line.
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
Polyline(Point::new_scale(10., -1.), Point::new_scale(0., -1.)) };
join_close_ends(lines, scale_(1.5), true);
REQUIRE(lines.size() == 1);
CHECK(lines.front().size() == 6);
// Three 10 mm lines and two 1 mm joins.
CHECK_THAT(unscale<double>(lines.front().length()), Catch::Matchers::WithinAbs(32., 1e-6));
}
TEST_CASE("Infill line ends are not joined across another line", "[Fill]")
{
// A pair 1 mm apart whose left ends have a third line running between them, which a join there would cross.
Polylines lines { Polyline(Point::new_scale(0., 0.), Point::new_scale(10., 0.)), Polyline(Point::new_scale(0., 1.), Point::new_scale(10., 1.)),
Polyline(Point::new_scale(-5., 0.5), Point::new_scale(5., 0.5)) };
join_close_ends(lines, scale_(1.5), true);
REQUIRE(lines.size() == 2);
const auto joined = std::find_if(lines.begin(), lines.end(), [](const Polyline &pl) { return pl.size() == 4; });
REQUIRE(joined != lines.end());
// Joined on the right, where nothing is in between, so the left ends stay open.
CHECK(joined->first_point().x() == 0);
CHECK(joined->last_point().x() == 0);
}
TEST_CASE("Infill lines that cannot reverse join only end to start", "[Fill]")
{
const Point a(Point::new_scale(0., 0.)), b(Point::new_scale(10., 0.)), c(Point::new_scale(10., 1.)), d(Point::new_scale(0., 1.));
SECTION("Ends that would need a reversal stay apart") {
Polylines lines { Polyline(a, b), Polyline(d, c) };
join_close_ends(lines, scale_(1.5), false);
CHECK(lines.size() == 2);
}
SECTION("The end of one line joins the start of the next, both kept in their direction") {
Polylines lines { Polyline(c, d), Polyline(a, b) };
join_close_ends(lines, scale_(1.5), false);
REQUIRE(lines.size() == 1);
const Points &points = lines.front().points;
REQUIRE(points.size() == 4);
auto position = [&points](const Point &p) { return std::find(points.begin(), points.end(), p) - points.begin(); };
CHECK(position(a) < position(b));
CHECK(position(c) < position(d));
}
}
+200
View File
@@ -2,15 +2,18 @@
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/catch_message.hpp>
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/MultiNozzleUtils.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "libslic3r/Utils.hpp"
#include "test_utils.hpp"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <cstdint>
@@ -30,6 +33,7 @@ namespace Slic3r { enum ExtrusionRole : uint8_t; }
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
using Catch::Matchers::WithinRel;
// Regression coverage for filament/tool-change time being folded into the first
// pending motion block (an extrusion move) instead of the tool-change move, and
@@ -570,6 +574,33 @@ double planned_corner_speed(GCodeFlavor flavor, double corner_velocity, double j
} // namespace
TEST_CASE("Klipper extrusion ratio changes use its own corner velocity, not E jerk", "[GCodeTiming][JunctionDeviation]")
{
const double jerk_e = GENERATE(0.0, 100.0);
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_jerk_e.values = {jerk_e, jerk_e};
GCodeProcessor processor;
run_processor(processor, config,
"G90\nM83\nSET_VELOCITY_LIMIT ACCEL=1000 MINIMUM_CRUISE_RATIO=0\n"
"G1 X20 Y60 F6000\nM400\n"
"G1 X60 Y60 E0.8 F6000\nG1 X100 Y60 E1.6 F6000\nM400\n");
// A 0.02 -> 0.04 extrusion ratio change is capped at 1 / 0.02 = 50 mm/s.
REQUIRE_THAT(corner_speed(processor.get_result()), Catch::Matchers::WithinAbs(50.0, 0.01));
}
TEST_CASE("Klipper legacy smoothing stays absolute across acceleration changes", "[GCodeTiming][Klipper]")
{
const bool modern = GENERATE(false, true);
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
const std::string gcode = std::string("G90\nSET_VELOCITY_LIMIT ACCEL=1000 ") +
(modern ? "MINIMUM_CRUISE_RATIO=0.75\n" : "ACCEL_TO_DECEL=250\n") +
"G1 X40 F30000\nM400\nM204 S2000\nG1 X0\nM400\n";
GCodeProcessor processor;
run_processor(processor, config, gcode.c_str());
const double expected = modern ? 0.5 + 0.25 * std::sqrt(2.0) : 0.95;
REQUIRE_THAT(processor.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinAbs(expected, 1e-5));
}
TEST_CASE("Klipper corners are planned with junction deviation derived from the square corner velocity",
"[GCodeTiming][JunctionDeviation]")
{
@@ -669,3 +700,172 @@ TEST_CASE("How fast a corner is taken does not depend on how much is extruded th
Catch::Matchers::WithinRel(marlin, 0.02));
}
}
// Expected times are Klipper's own planner math (klippy/toolhead.py). For a move from rest to rest,
// LookAheadQueue.flush() reduces to cruise_v2 = min(vmax^2, d * accel, d * pseudo_accel) with
// pseudo_accel = accel * (1 - minimum_cruise_ratio), and Move.set_junction() to a trapezoid at accel;
// a move too short to reach its speed still spends that ratio of its length cruising.
namespace {
constexpr double klipper_accel = 1000.0; // make_junction_config's acceleration
constexpr double klipper_cruise_ratio = 0.5;
constexpr double klipper_pseudo_accel = klipper_accel * (1.0 - klipper_cruise_ratio);
// Time of a move that starts and ends at rest.
double rest_to_rest_time(double distance, double max_speed, double accel, double pseudo_accel)
{
const double cruise_v2 = std::min(sqr(max_speed), distance * pseudo_accel);
const double cruise = std::sqrt(cruise_v2);
return 2.0 * cruise / accel + (distance - cruise_v2 / accel) / cruise;
}
// Every run states the cruise ratio rather than relying on the estimator's built-in default.
void run_klipper(GCodeProcessor& proc, const std::string& gcode,
const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
{
const std::string limits = "SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) + "\n";
run_processor(proc, config, (limits + gcode).c_str());
}
double klipper_time(const std::string& gcode, const FullPrintConfig& config = make_junction_config(gcfKlipper, 5.0, 0.0))
{
GCodeProcessor proc;
run_klipper(proc, gcode, config);
return proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal);
}
double time_after_retract(const GCodeProcessorResult& r)
{
double t = 0.0;
bool after = false;
for (const auto& mv : r.moves) {
if (mv.type == EMoveType::Retract)
after = true;
else if (after)
t += mv.time[NORMAL];
}
return t;
}
} // namespace
TEST_CASE("A straight line split into short segments is planned as one move", "[GCodeTiming][Klipper]")
{
// 100 mm at 50 mm/s queues 2 s of motion, so the look-ahead flushes lazily part-way along the
// line; at 10 mm/s^2 the ramps span many segments and the cruise ratio caps the speed.
const int segments = GENERATE(1, 100, 1000);
std::ostringstream gcode;
gcode << "SET_VELOCITY_LIMIT ACCEL=10\n" << std::fixed << std::setprecision(3);
for (int i = 1; i <= segments; ++i)
gcode << "G1 X" << 100.0 * i / segments << " F3000\n";
REQUIRE_THAT(klipper_time(gcode.str()),
WithinRel(rest_to_rest_time(100.0, 50.0, 10.0, 10.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
}
TEST_CASE("Where the look-ahead flushes does not change the estimate", "[GCodeTiming][Klipper]")
{
// A retraction stops the toolhead, so the zig-zag after it is planned on its own; the straight run
// before it only shifts where the queue flushes lazily (1 s of motion after a stop, then every 0.15 s).
auto zigzag_time = [](int run_moves) {
std::string gcode = "M83\nG91\n";
for (int i = 0; i < run_moves; ++i)
gcode += "G1 X1 F6000\n";
gcode += "G1 E-1 F2400\n";
const char* moves[] = {"G1 X2 F3000\n", "G1 Y2 F9000\n", "G1 X2 F1200\n", "G1 Y-2 F9000\n"};
for (int i = 0; i < 24; ++i)
gcode += moves[i % 4];
GCodeProcessor proc;
run_klipper(proc, gcode);
return time_after_retract(proc.get_result());
};
const double reference = zigzag_time(0);
REQUIRE(reference > 0.0);
for (int run_moves : {30, 80, 105, 130}) {
INFO("run moves = " << run_moves);
REQUIRE_THAT(zigzag_time(run_moves), WithinAbs(reference, 1e-9));
}
}
TEST_CASE("A short move into a corner is limited by the centripetal term, not the corner velocity",
"[GCodeTiming][Klipper]")
{
// A 50 mm/s square corner velocity allows 50 mm/s through the right angle; the 1 mm move into it
// allows sqrt(0.5 * 1 * 1000 * tan(45 deg)) = 22.4 mm/s (Move.calc_junction's move_centripetal_v2).
GCodeProcessor proc;
run_klipper(proc, "G90\nG1 X59 Y60 F6000\nM400\nG1 X60 Y60 F9000\nG1 X60 Y100 F9000\n",
make_junction_config(gcfKlipper, 50.0, 0.0));
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(std::sqrt(500.0), 1e-6));
}
TEST_CASE("A corner keeps the square corner velocity when the acceleration changes between its moves",
"[GCodeTiming][Klipper]")
{
GCodeProcessor proc;
run_klipper(proc, "G90\nG1 X20 Y60 F6000\nM400\nG1 X60 Y60 F9000\nM204 S4000\nG1 X60 Y100 F9000\n");
REQUIRE_THAT(corner_speed(proc.get_result()), WithinRel(5.0, 1e-6));
}
TEST_CASE("A retraction stops the toolhead and runs at the extruder's own limits", "[GCodeTiming][Klipper]")
{
// An E acceleration of 0 is unset, and the retraction then uses the retract acceleration.
const auto [e_accel, retract_accel] = GENERATE(table<double, double>({{20000.0, 20000.0}, {0.0, 3000.0}}));
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_acceleration_e.values = {e_accel, e_accel};
config.machine_max_acceleration_retracting.values = {3000.0, 3000.0};
GCodeProcessor proc;
run_klipper(proc, "G90\nM83\nG1 X60 Y60 F9000\nG1 E-1 F2400\nG1 X100 Y60 F9000\n", config);
REQUIRE_THAT(corner_speed(proc.get_result()), WithinAbs(0.0, 1e-6));
const double expected = rest_to_rest_time(std::sqrt(2.0) * 60.0, 150.0, klipper_accel, klipper_pseudo_accel) +
rest_to_rest_time(1.0, 40.0, retract_accel, std::min(retract_accel, klipper_pseudo_accel)) +
rest_to_rest_time(40.0, 150.0, klipper_accel, klipper_pseudo_accel);
REQUIRE_THAT(proc.get_time(PrintEstimatedStatistics::ETimeMode::Normal), WithinRel(expected, 1e-6));
}
TEST_CASE("Extruder limits apply to retractions, not to extrusion along a path", "[GCodeTiming][Klipper]")
{
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_speed_e.values = {10.0, 10.0};
config.machine_max_acceleration_e.values = {10.0, 10.0};
REQUIRE_THAT(klipper_time("M83\nG1 X40 E20 F30000\n", config),
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
REQUIRE_THAT(klipper_time("M83\nG1 E-1 F2400\n", config),
WithinRel(rest_to_rest_time(1.0, 10.0, 10.0, 10.0), 1e-6));
}
TEST_CASE("A Z move is limited by the Z axis speed and acceleration", "[GCodeTiming][Klipper]")
{
auto config = make_junction_config(gcfKlipper, 5.0, 0.0);
config.machine_max_speed_z.values = {20.0, 20.0};
config.machine_max_acceleration_z.values = {100.0, 100.0};
REQUIRE_THAT(klipper_time("G1 Z10 F1200\n", config),
WithinRel(rest_to_rest_time(10.0, 20.0, 100.0, 100.0), 1e-6));
}
TEST_CASE("M204 sets the acceleration from S, or the smaller of P and T", "[GCodeTiming][Klipper]")
{
const char* m204 = GENERATE("M204 S500", "M204 P500 T2000", "M204 P2000 T500");
REQUIRE_THAT(klipper_time(std::string(m204) + "\nG1 X100 F30000\n"),
WithinRel(rest_to_rest_time(100.0, 500.0, 500.0, 500.0 * (1.0 - klipper_cruise_ratio)), 1e-6));
}
TEST_CASE("SET_VELOCITY_LIMIT applies every limit on the line and ignores the comment", "[GCodeTiming][Klipper]")
{
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT VELOCITY=80 ACCEL=500 MINIMUM_CRUISE_RATIO=0.2 ; ACCEL=9999\n"
"G1 X20 F30000\n"),
WithinRel(rest_to_rest_time(20.0, 80.0, 500.0, 500.0 * (1.0 - 0.2)), 1e-6));
}
TEST_CASE("ACCEL_TO_DECEL is ignored on a line that also sets the minimum cruise ratio", "[GCodeTiming][Klipper]")
{
// Klipper 0.13 prefers the cruise ratio and later releases no longer read ACCEL_TO_DECEL at all.
// ACCEL_TO_DECEL comes last, so applying the parameters in line order would let it win.
REQUIRE_THAT(klipper_time("SET_VELOCITY_LIMIT MINIMUM_CRUISE_RATIO=" + std::to_string(klipper_cruise_ratio) +
" ACCEL_TO_DECEL=100\nG1 X40 F30000\n"),
WithinRel(rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
}
TEST_CASE("M400 without a delay waits for the queue to drain", "[GCodeTiming][Klipper]")
{
REQUIRE_THAT(klipper_time("G90\nG1 X40 F30000\nM400\nG1 X80\n"),
WithinRel(2.0 * rest_to_rest_time(40.0, 500.0, klipper_accel, klipper_pseudo_accel), 1e-6));
}
+36 -1
View File
@@ -514,7 +514,7 @@ TEST_CASE("Acceleration and velocity limit commands print their values in genera
const Case c = GENERATE(values<Case>({
{gcfKlipper, Command::KlipperLimits, 2000000, 25. / 3., false,
{"SET_VELOCITY_LIMIT ACCEL=2000000 ", "ACCEL_TO_DECEL=1e+06 ", "SQUARE_CORNER_VELOCITY=8.33333\n"}, {}},
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
{gcfKlipper, Command::KlipperLimits, 12345, 0., false, {"ACCEL=12345 ", "ACCEL_TO_DECEL=6172.5 ", "MINIMUM_CRUISE_RATIO=0.5\n"}, {"SQUARE_CORNER_VELOCITY"}},
{gcfKlipper, Command::KlipperLimits, 0, 0.25, true, {"SQUARE_CORNER_VELOCITY=0.25 ", "; adjust VELOCITY_LIMIT"}, {"ACCEL"}},
{gcfKlipper, Command::Print, 3001, 0., true, {"ACCEL=3001 ", "ACCEL_TO_DECEL=1500.5 ", "; adjust ACCEL_TO_DECEL", "; adjust acceleration"}, {}},
{gcfMarlinFirmware, Command::Print, 2500, 0., false, {"M204 P2500\n"}, {}},
@@ -1272,6 +1272,41 @@ TEST_CASE("Custom G-code motion limits are restored before generated moves", "[G
REQUIRE(gcode.find("M205 X8 Y8 ; adjust jerk", custom_gcode_pos) != std::string::npos);
}
TEST_CASE("Klipper acceleration commands send accel_to_decel both as ACCEL_TO_DECEL and as the cruise ratio", "[GCodeWriter]")
{
// Klipper 0.13 defines MINIMUM_CRUISE_RATIO = 1 - ACCEL_TO_DECEL / ACCEL; older releases read only the
// first parameter and current ones only the second, so both have to describe the same limit.
GCodeWriter writer;
writer.config.gcode_flavor.value = gcfKlipper;
writer.config.accel_to_decel_enable.value = true;
// The writer appends "; adjust ..." comments by default; keep them out so the line ends at the ratio.
struct CommentGuard
{
bool saved = GCodeWriter::full_gcode_comment;
~CommentGuard() { GCodeWriter::full_gcode_comment = saved; }
} comment_guard;
GCodeWriter::full_gcode_comment = false;
SECTION("a 25% factor") {
writer.config.accel_to_decel_factor.value = 25;
REQUIRE_THAT(writer.set_print_acceleration(2000),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=500 MINIMUM_CRUISE_RATIO=0.75"));
REQUIRE_THAT(writer.set_accel_and_jerk(3000, 0.0),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=3000 ACCEL_TO_DECEL=750 MINIMUM_CRUISE_RATIO=0.75"));
}
SECTION("a 100% factor turns the cruise ratio off") {
writer.config.accel_to_decel_factor.value = 100;
REQUIRE_THAT(writer.set_print_acceleration(2000),
Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000 ACCEL_TO_DECEL=2000 MINIMUM_CRUISE_RATIO=0\n"));
}
SECTION("neither is sent while the option is off") {
writer.config.accel_to_decel_enable.value = false;
const std::string line = writer.set_print_acceleration(2000);
REQUIRE_THAT(line, Catch::Matchers::StartsWith("SET_VELOCITY_LIMIT ACCEL=2000"));
REQUIRE_THAT(line, !Catch::Matchers::ContainsSubstring("ACCEL_TO_DECEL") && !Catch::Matchers::ContainsSubstring("MINIMUM_CRUISE_RATIO"));
}
}
// Regression test for the belt-printer "illegal gantry move at print start" bug.
//
// On a belt printer the layer-change z-hop is deferred (lazy_lift) and consumed
+17
View File
@@ -202,6 +202,23 @@ TEST_CASE("The wipe tower's toolchange planner flush follows the gcode flavor",
}
}
TEST_CASE("The wipe tower sets Klipper's acceleration and accel_to_decel on one line", "[WipeTower]")
{
// On separate lines, current Klipper finds nothing it reads on the ACCEL_TO_DECEL one and 0.13
// derives the cruise ratio from the acceleration before the change.
DynamicPrintConfig config = wipe_tower_toolchange_config("klipper");
// Type 2 towers leave the acceleration to the main G-code writer.
config.set_deserialize_strict({ { "wipe_tower_type", "type1" },
{ "accel_to_decel_enable", "1" }, { "accel_to_decel_factor", "50%" },
{ "default_acceleration", "1000" }, { "travel_acceleration", "2000" },
{ "machine_max_acceleration_extruding", "5000" } });
const std::string tower = wipe_tower_regions(slice_with_prime_tower(config));
REQUIRE_THAT(tower, Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL="));
CHECK_THAT(tower, Catch::Matchers::ContainsSubstring(" MINIMUM_CRUISE_RATIO=0.5\n"));
CHECK_THAT(tower, !Catch::Matchers::ContainsSubstring("SET_VELOCITY_LIMIT ACCEL_TO_DECEL") &&
!Catch::Matchers::ContainsSubstring("M204"));
}
// What Print feeds the shared estimate. The libslic3r WipeTowerEstimate cases cannot see this:
// they call the estimator directly. The estimate counts the filaments the print really uses,
// so the two-filament shape gives the outer wall the second one.
+8 -1
View File
@@ -1,5 +1,9 @@
get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
# Observe the geometry backend's actual CGAL layout without duplicating its flags.
# This source is only added when the test suite is enabled.
target_sources(libslic3r_cgal PRIVATE texture_import_cgal.cpp)
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_3mf.cpp
@@ -31,11 +35,12 @@ add_executable(${_TEST_NAME}_tests
test_preset_diff.cpp
test_vendor_cache.cpp
test_preset_options.cpp
test_edge_grid.cpp
test_elephant_foot_compensation.cpp
test_fill_corner_smoothing.cpp
test_filament_mixer.cpp
test_fill_plane_path.cpp
test_fill_tpms_adaptive.cpp
test_adaptive_infill_field.cpp
test_geometry.cpp
test_imex_arrange.cpp
test_imex_helpers.cpp
@@ -67,10 +72,12 @@ add_executable(${_TEST_NAME}_tests
test_wipe_path.cpp
test_optimizers.cpp
test_ordering_strategies.cpp
test_shortest_path.cpp
# test_png_io.cpp
test_indexed_triangle_set.cpp
test_connected_bodies.cpp
test_texture_displacement.cpp
test_texture_import.cpp
test_instance_lock.cpp
../libnest2d/printer_parts.cpp
)
@@ -13,7 +13,7 @@
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Fill/AdaptiveInfillField.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
@@ -30,24 +30,24 @@ ExPolygon rectangle(double x0, double y0, double x1, double y1)
}
// The expolygons stacked in 0.2 mm layers from z = 0 to height.
TpmsRadialField radial_field(const ExPolygons &expolygons, double height, TpmsAdaptiveMode mode = TpmsAdaptiveMode::Lobes)
AdaptiveInfillField radial_field(const ExPolygons &expolygons, double height, AdaptiveInfillMode mode = AdaptiveInfillMode::Lobes)
{
std::vector<TpmsRadialField::Slice> slices;
std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons});
return TpmsRadialField(slices, get_extents(expolygons), mode, [] {});
return AdaptiveInfillField(slices, get_extents(expolygons), mode, [] {});
}
double radial(const TpmsRadialField &field, const Vec3d &pt)
double radial(const AdaptiveInfillField &field, const Vec3d &pt)
{
TpmsRadialField::Radials radials;
AdaptiveInfillField::Radials radials;
field.radial(pt, radials);
return radials[0].t;
}
Vec3d center(const TpmsRadialField &field, const Vec3d &pt)
Vec3d center(const AdaptiveInfillField &field, const Vec3d &pt)
{
TpmsRadialField::Radials radials;
AdaptiveInfillField::Radials radials;
field.radial(pt, radials);
return radials[0].center;
}
@@ -57,11 +57,11 @@ constexpr double Tolerance = 0.075;
} // namespace
TEST_CASE("TPMS radial field is zero at the center of a cube and one at its faces", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field is zero at the center of a cube and one at its faces", "[AdaptiveInfillField]")
{
// A 20 mm cube, 10 mm from its center to every face.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
const AdaptiveInfillField field = radial_field(square, 20.);
const Vec3d c = center(field, {10., 10., 10.});
CHECK_THAT(c.x(), WithinAbs(10., 0.5));
@@ -74,10 +74,10 @@ TEST_CASE("TPMS radial field is zero at the center of a cube and one at its face
}
}
TEST_CASE("TPMS radial field grows linearly from the center of a cube to its faces", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field grows linearly from the center of a cube to its faces", "[AdaptiveInfillField]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
const AdaptiveInfillField field = radial_field(square, 20.);
for (double d = 1.; d < 10.; d += 1.) {
CAPTURE(d);
CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance));
@@ -85,11 +85,11 @@ TEST_CASE("TPMS radial field grows linearly from the center of a cube to its fac
}
}
TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field of a tall box is centered at its middle height", "[AdaptiveInfillField]")
{
// 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60.);
const AdaptiveInfillField field = radial_field(square, 60.);
CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5));
CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance));
@@ -97,10 +97,10 @@ TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[
CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance));
}
TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field grades every body towards its own center", "[AdaptiveInfillField]")
{
const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)};
const TpmsRadialField field = radial_field(squares, 20.);
const AdaptiveInfillField field = radial_field(squares, 20.);
CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5));
CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5));
@@ -108,20 +108,20 @@ TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTp
CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance));
}
TEST_CASE("TPMS radial field is beyond one outside of the object", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field is beyond one outside of the object", "[AdaptiveInfillField]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
const AdaptiveInfillField field = radial_field(square, 20.);
CHECK(radial(field, {-5., 10., 10.}) > 1.);
CHECK(radial(field, {10., 10., 30.}) > 1.);
}
TEST_CASE("TPMS radial field grades every lobe of a body towards its own center", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field grades every lobe of a body towards its own center", "[AdaptiveInfillField]")
{
// Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep.
const Vec3d c1(10., 10., 10.), c2(26., 10., 10.);
std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices;
std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
@@ -134,7 +134,7 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
for (const Vec3d &c : {c1, c2}) {
CAPTURE(c.x());
@@ -143,7 +143,7 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance));
}
// The side between the lobes is half way to the surface, where both patterns morph into each other.
TpmsRadialField::Radials radials;
AdaptiveInfillField::Radials radials;
REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2);
for (size_t i = 0; i < 2; ++i) {
CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance));
@@ -151,12 +151,12 @@ TEST_CASE("TPMS radial field grades every lobe of a body towards its own center"
}
}
TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field blends the lobes meeting at a junction continuously", "[AdaptiveInfillField]")
{
// Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle.
const std::array<Vec3d, 3> centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)};
std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices;
std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
@@ -169,7 +169,7 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
// Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly.
const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.;
@@ -179,7 +179,7 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
for (int row = 0; row <= 100; ++row) {
std::array<float, 3> previous{};
for (int step = 0; step <= 200; ++step) {
TpmsRadialField::Radials radials;
AdaptiveInfillField::Radials radials;
const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials);
max_count = std::max(max_count, count);
std::array<float, 3> weights{};
@@ -201,23 +201,23 @@ TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously
CHECK(max_jump < 0.05);
}
TEST_CASE("TPMS radial field is empty when the object is thinner than the grid cells", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field is empty when the object is thinner than the grid cells", "[AdaptiveInfillField]")
{
// A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more.
const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)};
std::vector<TpmsRadialField::Slice> slices;
std::vector<AdaptiveInfillField::Slice> slices;
for (int i = 0; i < 1000; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &bar});
const TpmsRadialField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
TpmsAdaptiveMode::Lobes, [] {});
const AdaptiveInfillField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
AdaptiveInfillMode::Lobes, [] {});
CHECK(field.empty());
}
TEST_CASE("TPMS depth follows the distance to the surface relative to the deepest point", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill depth follows the distance to the surface relative to the deepest point", "[AdaptiveInfillField]")
{
// 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60., TpmsAdaptiveMode::SmoothBlend);
const AdaptiveInfillField field = radial_field(square, 60., AdaptiveInfillMode::SmoothBlend);
for (double z : {15., 30., 45.}) {
CAPTURE(z);
CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance));
@@ -227,18 +227,18 @@ TEST_CASE("TPMS depth follows the distance to the surface relative to the deepes
CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance));
}
TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the surface", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field in Distance warp mode follows the distance to the surface", "[AdaptiveInfillField]")
{
// In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField cube = radial_field(square, 20., TpmsAdaptiveMode::DistanceWarp);
const AdaptiveInfillField cube = radial_field(square, 20., AdaptiveInfillMode::DistanceWarp);
CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance));
// 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep.
const TpmsRadialField lobes = radial_field(square, 60.);
const TpmsRadialField warp = radial_field(square, 60., TpmsAdaptiveMode::DistanceWarp);
const AdaptiveInfillField lobes = radial_field(square, 60.);
const AdaptiveInfillField warp = radial_field(square, 60., AdaptiveInfillMode::DistanceWarp);
for (double z : {15., 45.}) {
CAPTURE(z);
CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance));
@@ -247,12 +247,12 @@ TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the s
CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance));
}
TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill stepped shells split a layer by depth from the surface inwards", "[AdaptiveInfillField]")
{
// The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside.
const ExPolygons square{rectangle(0., 0., 40., 40.)};
const TpmsRadialField field = radial_field(square, 40., TpmsAdaptiveMode::SteppedShells);
const std::vector<TpmsShell> shells = make_tpms_shells(field, square.front(), 20., 0.2f, 0.05f, TpmsAdaptiveGradient::Linear);
const AdaptiveInfillField field = radial_field(square, 40., AdaptiveInfillMode::SteppedShells);
const std::vector<AdaptiveInfillShell> shells = make_adaptive_infill_shells(field, square.front(), 20., 0.2f, 0.05f, AdaptiveInfillGradient::Linear);
REQUIRE(shells.size() == 5);
CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6));
CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6));
@@ -270,11 +270,11 @@ TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards",
[&center](const ExPolygon &expolygon) { return expolygon.contains(center); }));
}
TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on its own", "[FillTpmsAdaptive]")
TEST_CASE("Adaptive infill field in 2D grades every section normal to the axis on its own", "[AdaptiveInfillField]")
{
// 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField normal_z = radial_field(square, 60., TpmsAdaptiveMode::NormalZ);
const AdaptiveInfillField normal_z = radial_field(square, 60., AdaptiveInfillMode::NormalZ);
for (double z : {5., 30., 55.}) {
CAPTURE(z);
CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance));
@@ -283,7 +283,7 @@ TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on it
}
// Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const TpmsRadialField normal_x = radial_field(square, 60., TpmsAdaptiveMode::NormalX);
const AdaptiveInfillField normal_x = radial_field(square, 60., AdaptiveInfillMode::NormalX);
for (double x : {3., 10., 17.}) {
CAPTURE(x);
CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance));
+13
View File
@@ -1457,6 +1457,19 @@ TEST_CASE("min_object_distance yields no floor when an FFF config lacks the opti
}
}
TEST_CASE("handle_legacy migrates the adaptive TPMS keys", "[Config]") {
ConfigSubstitutionContext ctxt(ForwardCompatibilitySubstitutionRule::Enable);
DynamicPrintConfig config;
config.set_deserialize("tpms_adaptive", "stepped_shells", ctxt);
config.set_deserialize("tpms_interior_density", "12%", ctxt);
config.set_deserialize("tpms_adaptive_gradient", "quadratic", ctxt);
CHECK(config.opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") == AdaptiveInfillMode::SteppedShells);
CHECK(config.option<ConfigOptionPercent>("adaptive_infill_interior_density")->value == Catch::Approx(12.));
CHECK(config.opt_enum<AdaptiveInfillGradient>("adaptive_infill_gradient") == AdaptiveInfillGradient::Quadratic);
CHECK(ctxt.substitutions.empty());
}
TEST_CASE("handle_legacy migrates every IDEX/IQEX key testers already have", "[Config]") {
// Three eras of saved profiles: the feature shipped as iXex (is_ixex + ixex_*), the clearance
// pair was renamed to say what it measures, then the whole prefix became imex_. Driven from the
+33
View File
@@ -0,0 +1,33 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "catch2/matchers/catch_matchers.hpp"
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/EdgeGrid.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
TEST_CASE("EdgeGrid propagates signed distances across the interior and exterior", "[EdgeGrid][Regression]")
{
const Polygon rectangle {Point::new_scale(-10., -12.), Point::new_scale(10., -12.),
Point::new_scale(10., 12.), Point::new_scale(-10., 12.)};
const Polygons outlines {rectangle};
const BoundingBox bounds(Point::new_scale(-30., -32.), Point::new_scale(30., 32.));
EdgeGrid::Grid grid(bounds);
grid.create(outlines, scale_(1.));
// Distances beyond the initialized edge band require backward propagation.
// With -fsanitize=shift, this also checks negative neighbour offsets.
grid.calculate_sdf();
const auto distance = [&](double x, double y) {
return grid.signed_distance_bilinear(Point::new_scale(x, y)) * SCALING_FACTOR;
};
REQUIRE_THAT(distance(0., 0.), Catch::Matchers::WithinAbs(-10., 0.01));
for (double sign : {-1., 1.}) {
REQUIRE_THAT(distance(sign * 5., 0.), Catch::Matchers::WithinAbs(-5., 0.01));
REQUIRE_THAT(distance(0., sign * 7.), Catch::Matchers::WithinAbs(-5., 0.01));
REQUIRE_THAT(distance(sign * 20., 0.), Catch::Matchers::WithinAbs(10., 0.01));
REQUIRE_THAT(distance(0., sign * 22.), Catch::Matchers::WithinAbs(10., 0.01));
}
}
@@ -1,4 +1,5 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_all.hpp>
#include <vector>
#include <cstddef>
@@ -12,10 +13,13 @@
#include "libslic3r/Point.hpp"
#include "libslic3r/GCode/OrderingStrategies.hpp"
#include "libslic3r/ShortestPath.hpp"
#include "libslic3r/Geometry.hpp"
#include <algorithm>
#include <unordered_set>
#include <numeric>
#include <random>
#include <catch2/catch_approx.hpp>
using namespace Slic3r;
@@ -304,3 +308,60 @@ TEST_CASE("snake_core handles three points", "[Snake]") {
REQUIRE(is_permutation(p2, 3));
}
// The shortest cycle through the points, trying every order.
static double shortest_cycle_length(const Points& centers)
{
std::vector<size_t> path(centers.size());
std::iota(path.begin(), path.end(), 0);
double best = std::numeric_limits<double>::max();
do {
best = std::min(best, tsp_cycle_path_length(path, centers));
} while (std::next_permutation(path.begin() + 1, path.end()));
return best;
}
static Points random_centers(unsigned seed, size_t count)
{
std::mt19937 rng(seed);
std::uniform_real_distribution<> coordinate(0., 200.);
Points centers;
for (size_t i = 0; i < count; ++i) {
// One draw per statement, as the evaluation order of function arguments is unspecified.
const double x = coordinate(rng);
const double y = coordinate(rng);
centers.emplace_back(Point::new_scale(x, y));
}
return centers;
}
TEST_CASE("tsp_exact_cycle finds the shortest cycle", "[TSPPostProcessing]") {
const size_t count = GENERATE(4, 5, 6, 8);
for (unsigned seed = 1; seed <= 10; ++seed) {
const Points centers = random_centers(seed, count);
const std::vector<size_t> path = tsp_exact_cycle(centers);
REQUIRE(is_permutation(path, count));
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
}
}
TEST_CASE("Default ordering tours up to ten points along the shortest cycle", "[TSPPostProcessing]") {
const bool from_start = GENERATE(false, true);
const Point start = Point::new_scale(-20., -20.);
for (unsigned seed = 1; seed <= 5; ++seed) {
const Points centers = random_centers(seed, 9);
const std::vector<size_t> path = chain_points_with_postprocessing(centers, from_start ? &start : nullptr);
REQUIRE(is_permutation(path, centers.size()));
CHECK(euclidean_path_length(path, centers) <= shortest_cycle_length(centers) + SCALED_EPSILON);
}
}
TEST_CASE("Default ordering of many points is never longer than the greedy cycle", "[TSPPostProcessing]") {
for (unsigned seed = 1; seed <= 10; ++seed) {
const Points centers = random_centers(seed, 150);
const std::vector<size_t> path = chain_points_with_postprocessing(centers, nullptr);
REQUIRE(is_permutation(path, centers.size()));
CHECK(euclidean_path_length(path, centers) <= euclidean_path_length(chain_points(centers, nullptr), centers) + SCALED_EPSILON);
CHECK_FALSE(has_crossings(path, centers));
}
}
+202
View File
@@ -0,0 +1,202 @@
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/ExtrusionEntity.hpp"
#include "libslic3r/ExtrusionEntityCollection.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polyline.hpp"
#include "libslic3r/ShortestPath.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cmath>
#include <cstddef>
#include <limits>
#include <map>
#include <numeric>
#include <math.h>
#include <random>
#include <utility>
#include <vector>
using namespace Slic3r;
namespace {
// Infill-like paths: lines of 1 to 30 mm scattered over a 100 mm square. The collection owns them, and entities hands
// them out as the raw pointers chain_and_reorder_extrusion_entities() reorders.
struct Paths
{
ExtrusionEntityCollection collection;
std::vector<ExtrusionEntity*> entities;
Paths(unsigned seed, size_t count, bool can_reverse)
{
std::mt19937 rng(seed);
std::uniform_real_distribution<> coordinate(0., 100.), length(1., 30.), angle(0., 2. * M_PI);
Polylines lines;
for (size_t i = 0; i < count; ++ i) {
// One draw per statement, as the evaluation order of function arguments is unspecified.
const double x = coordinate(rng);
const double y = coordinate(rng);
const double l = length(rng);
const double t = angle(rng);
const Vec2d a(x, y);
const Vec2d b = a + l * Vec2d(std::cos(t), std::sin(t));
lines.emplace_back(Point::new_scale(a), Point::new_scale(b));
}
extrusion_entities_append_paths(collection.entities, std::move(lines), erInternalInfill, 0.1, 0.45f, 0.2f, can_reverse);
entities = collection.entities;
}
};
double travel(const std::vector<ExtrusionEntity*> &entities, const Point &start)
{
double length = 0.;
Point last = start;
for (const ExtrusionEntity *ee : entities) {
length += (ee->first_point() - last).cast<double>().norm();
last = ee->last_point();
}
return length;
}
// Travel of the chain that always goes on to the nearest free path end.
double nearest_next_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
{
std::vector<char> taken(entities.size(), 0);
double length = 0.;
Point last = from;
for (size_t step = 0; step < entities.size(); ++ step) {
double best = std::numeric_limits<double>::max();
size_t best_idx = 0;
bool best_reversed = false;
for (size_t i = 0; i < entities.size(); ++ i)
for (const bool reversed : { false, true })
if (! taken[i] && (can_reverse || ! reversed)) {
const double d = ((reversed ? entities[i]->last_point() : entities[i]->first_point()) - last).cast<double>().norm();
if (d < best) {
best = d;
best_idx = i;
best_reversed = reversed;
}
}
taken[best_idx] = 1;
length += best;
last = best_reversed ? entities[best_idx]->first_point() : entities[best_idx]->last_point();
}
return length;
}
// The shortest travel over every order and allowed reversal of the paths, from start.
double shortest_travel(const std::vector<ExtrusionEntity*> &entities, bool can_reverse, const Point &from)
{
std::vector<size_t> order(entities.size());
std::iota(order.begin(), order.end(), 0);
double best = std::numeric_limits<double>::max();
do {
for (size_t mask = 0; mask < (size_t(1) << order.size()) && (can_reverse || mask == 0); ++ mask) {
double length = 0.;
Point last = from;
for (size_t k = 0; k < order.size(); ++ k) {
const ExtrusionEntity *ee = entities[order[k]];
const bool reversed = (mask >> k & 1) != 0;
length += ((reversed ? ee->last_point() : ee->first_point()) - last).cast<double>().norm();
last = reversed ? ee->first_point() : ee->last_point();
}
best = std::min(best, length);
}
} while (std::next_permutation(order.begin(), order.end()));
return best;
}
const Point start = Point::new_scale(-10., -10.);
} // namespace
TEST_CASE("An improved infill chain is never longer than the greedy or nearest-next one and keeps every path", "[ShortestPath]")
{
const size_t count = GENERATE(2, 3, 10, 60, 300);
const bool can_reverse = GENERATE(true, false);
double greedy_total = 0., improved_total = 0.;
for (unsigned seed = 1; seed <= 20; ++ seed) {
Paths greedy(seed, count, can_reverse), improved(seed, count, can_reverse);
const double nearest_travel = nearest_next_travel(improved.entities, can_reverse, start);
chain_and_reorder_extrusion_entities(greedy.entities, &start);
chain_and_reorder_extrusion_entities(improved.entities, start);
std::vector<ExtrusionEntity*> sorted = improved.entities;
std::sort(sorted.begin(), sorted.end());
REQUIRE(std::unique(sorted.begin(), sorted.end()) == sorted.end());
REQUIRE(sorted.size() == count);
const double greedy_travel = travel(greedy.entities, start), improved_travel = travel(improved.entities, start);
CHECK(improved_travel <= greedy_travel + SCALED_EPSILON);
CHECK(improved_travel <= nearest_travel + SCALED_EPSILON);
greedy_total += greedy_travel;
improved_total += improved_travel;
}
// Twenty random sets of ten or more paths always leave the greedy chain something to shorten.
if (count >= 10)
CHECK(improved_total < 0.95 * greedy_total);
}
TEST_CASE("Improving an infill chain keeps the direction of paths that cannot reverse", "[ShortestPath]")
{
Paths paths(7, 100, false);
std::map<const ExtrusionEntity*, Point> first_points;
for (const ExtrusionEntity *ee : paths.entities)
first_points.emplace(ee, ee->first_point());
chain_and_reorder_extrusion_entities(paths.entities, start);
for (const ExtrusionEntity *ee : paths.entities)
CHECK(ee->first_point() == first_points.at(ee));
}
TEST_CASE("Chains of a few paths are the shortest possible", "[ShortestPath]")
{
const size_t count = GENERATE(2, 3, 4, 5, 6);
const bool can_reverse = GENERATE(true, false);
for (unsigned seed = 1; seed <= 10; ++ seed) {
Paths paths(seed, count, can_reverse);
const double shortest = shortest_travel(paths.entities, can_reverse, start);
chain_and_reorder_extrusion_entities(paths.entities, start);
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
}
}
TEST_CASE("The greedy chain of two to four paths is the shortest possible", "[ShortestPath]")
{
const size_t count = GENERATE(2, 3, 4);
const bool can_reverse = GENERATE(true, false);
for (unsigned seed = 1; seed <= 10; ++ seed) {
Paths paths(seed, count, can_reverse);
const double shortest = shortest_travel(paths.entities, can_reverse, start);
chain_and_reorder_extrusion_entities(paths.entities, &start);
CHECK(travel(paths.entities, start) <= shortest + SCALED_EPSILON);
}
}
TEST_CASE("A short chain need not start with the path nearest to its start", "[ShortestPath]")
{
// The nearest end leads 10 mm away from the two other paths, so the shortest chain leaves that path for last.
const Point from = Point::new_scale(0., 0.);
for (const bool greedy : { false, true }) {
ExtrusionEntityCollection collection;
extrusion_entities_append_paths(collection.entities,
Polylines{ Polyline(Point::new_scale(-0.9, 0.), Point::new_scale(-10., 0.)), Polyline(Point::new_scale(1., 0.), Point::new_scale(2., 0.)),
Polyline(Point::new_scale(3., 1.), Point::new_scale(4., 1.)) },
erInternalInfill, 0.1, 0.45f, 0.2f);
std::vector<ExtrusionEntity*> entities = collection.entities;
const double shortest = shortest_travel(entities, true, from);
if (greedy)
chain_and_reorder_extrusion_entities(entities, &from);
else
chain_and_reorder_extrusion_entities(entities, from);
CHECK(entities.back() == collection.entities.front());
CHECK_THAT(travel(entities, from), Catch::Matchers::WithinAbs(shortest, SCALED_EPSILON));
}
}
+76
View File
@@ -0,0 +1,76 @@
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include "libslic3r/Model.hpp"
#include "libslic3r/TexturePainting.hpp"
#include "texture_import_cgal.hpp"
#include <algorithm>
#include <array>
#include <string>
#include <vector>
namespace {
void check_texture_import(const std::string& fixture)
{
const std::string filename = std::string(TEST_DATA_DIR) + "/texture_import/" + fixture;
INFO(filename);
const Slic3r::Model model = Slic3r::Model::read_from_file(filename);
REQUIRE_FALSE(model.objects.empty());
REQUIRE(model.texture_mesh);
const Slic3r::TexturedMesh& mesh = *model.texture_mesh;
Slic3r::TexturePaintingSettings settings;
settings.target_colors_num = 0; // Match the dialog's initial automatic color count.
settings.smooth_weight = 0.5;
settings.mesh_repair_decision = Slic3r::TexturePaintingSettings::MeshRepairDecision::ImportWithoutRepair;
Slic3r::PaintedMesh painted;
// Follow the dialog worker's format-dependent path, including the boundary
// smoothing that needs a consistent CGAL Kd_tree layout across libraries.
if (!mesh.precomputed_face_colors.empty())
REQUIRE(Slic3r::face_colors_to_painting(mesh, painted, settings));
else
REQUIRE(Slic3r::texture_to_painting(mesh, painted, settings));
REQUIRE_FALSE(painted.indices.empty());
REQUIRE(painted.face_colors.size() == painted.indices.size());
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
[](const auto& color) { return color[0] > color[2]; }));
REQUIRE(std::any_of(painted.face_colors.begin(), painted.face_colors.end(),
[](const auto& color) { return color[2] > color[0]; }));
const std::vector<std::array<float, 4>> filament_colors{
{1.f, 0.f, 0.f, 1.f}, {0.f, 0.f, 1.f, 1.f}};
const auto matches = Slic3r::match_clusters_to_filaments(
painted.cluster_colors, filament_colors, {"Red PLA", "Blue PLA"});
REQUIRE(matches.size() == painted.cluster_colors.size());
REQUIRE(std::any_of(matches.begin(), matches.end(),
[](const auto& match) { return match.filament_index == 0; }));
REQUIRE(std::any_of(matches.begin(), matches.end(),
[](const auto& match) { return match.filament_index == 1; }));
}
} // namespace
TEST_CASE("Color-boundary smoothing shares its CGAL tree layout with the geometry backend", "[TextureImport][CGAL][Regression]")
{
// An ABI mismatch need not crash every link of the importer. Compare the
// layouts directly so a lucky choice of inline symbols cannot mask it.
REQUIRE(sizeof(TextureImportTest::SearchTree) == TextureImportTest::backend_search_tree_size());
}
TEST_CASE("A JPEG textured GLB retains both colors through filament matching", "[TextureImport][Regression]")
{
check_texture_import("two_color_jpeg.glb");
}
TEST_CASE("An OBJ preserves both MTL colors through filament matching", "[TextureImport][OBJ][Regression]")
{
check_texture_import("two_color_sphere/test_two_color_sphere.obj");
}
TEST_CASE("An OBJ resolves its MTL and external JPEG through filament matching", "[TextureImport][OBJ][Regression]")
{
check_texture_import("two_color_sphere/test_two_color_sphere_textured.obj");
}
+8
View File
@@ -0,0 +1,8 @@
#include "texture_import_cgal.hpp"
#include <cstddef>
std::size_t TextureImportTest::backend_search_tree_size()
{
return sizeof(SearchTree);
}
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#include <CGAL/AABB_face_graph_triangle_primitive.h>
#include <CGAL/AABB_traits_3.h>
#include <CGAL/Exact_predicates_inexact_constructions_kernel.h>
#include <CGAL/Kd_tree.h>
#include <CGAL/Search_traits_adapter.h>
#include <CGAL/Surface_mesh/Surface_mesh.h>
#include <CGAL/property_map.h>
#include <cstddef>
namespace TextureImportTest {
// The distance-query tree used by ColorUtils.cpp's boundary smoothing. Its
// type name stays the same when CGAL changes its storage for TBB support.
using Kernel = CGAL::Exact_predicates_inexact_constructions_kernel;
using Mesh = CGAL::Surface_mesh<Kernel::Point_3>;
using Primitive = CGAL::AABB_face_graph_triangle_primitive<Mesh>;
using Traits = CGAL::AABB_traits_3<Kernel, Primitive>;
using PointId = Traits::Point_and_primitive_id;
using SearchTraits = CGAL::Search_traits_adapter<PointId, CGAL::First_of_pair_property_map<PointId>, Traits>;
using SearchTree = CGAL::Kd_tree<SearchTraits>;
// Implemented inside the actual geometry target, using its private definitions.
std::size_t backend_search_tree_size();
} // namespace TextureImportTest
+1
View File
@@ -44,6 +44,7 @@ add_executable(${_TEST_NAME}_tests
test_shortcuts.cpp
test_texture_color_palette.cpp
test_texture_island_view.cpp
test_timelapse_codecs.cpp
test_file_url.cpp
test_imgui_wrapper.cpp
test_user_manager.cpp
@@ -81,3 +81,24 @@ TEST_CASE("Payload rewrite covers nested trays, calibration lists and mapping in
CHECK(BBLPrinterAgent::from_orca_payload("not json") == "not json");
CHECK(BBLPrinterAgent::from_orca_payload(R"({"print":{"command":"pushall"}})") == R"({"print":{"command":"pushall"}})");
}
TEST_CASE("Payload rewrite covers the nozzle rack status and the auto nozzle mapping request", "[BambuFilamentIds]")
{
const ScopedRepoResourcesDir repo_resources;
const std::string petg_hf = orca_id_of("GFG96");
const std::string rack_status =
R"({"print":{"device":{"nozzle":{"info":[{"id":16,"fila_id":"GFG96","color_m":"FFFFFFFF"},{"id":17,"fila_id":"P22f54c9"}]}}}})";
json inbound = json::parse(BBLPrinterAgent::to_orca_payload(rack_status));
CHECK(inbound["print"]["device"]["nozzle"]["info"][0]["fila_id"] == petg_hf);
CHECK(inbound["print"]["device"]["nozzle"]["info"][1]["fila_id"] == "P22f54c9"); // user id: untouched
const std::string request = R"({"print":{"command":"get_auto_nozzle_mapping","ams_mapping":[65535,65024],)"
R"("fila_info":[{"id":2,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"}],)"
R"("nozzle_info":[{"pos":16,"cate":")" + petg_hf + R"(","color":"FFFFFFFF"},{"pos":1,"cate":""}]}})";
json outbound = json::parse(BBLPrinterAgent::from_orca_payload(request));
CHECK(outbound["print"]["fila_info"][0]["cate"] == "GFG96");
CHECK(outbound["print"]["nozzle_info"][0]["cate"] == "GFG96");
CHECK(outbound["print"]["nozzle_info"][1]["cate"] == ""); // empty nozzle
CHECK(outbound["print"]["ams_mapping"] == json::parse("[65535,65024]"));
}
@@ -420,12 +420,13 @@ TEST_CASE("Moonraker discovery waits for a complete device identity", "[Moonrake
agent.device_info.dev_id = "127.0.0.1:7125";
agent.device_info.dev_ip = agent.device_info.dev_id;
agent.device_info.base_url = "http://127.0.0.1:7125";
agent.device_info.model_name = "Test model";
agent.start_discovery(true, false);
REQUIRE(announcements == 1);
REQUIRE(agent.fetch_count == 1);
REQUIRE(agent.fetch_count == 0);
REQUIRE(announcement.at("dev_id") == agent.device_info.dev_id);
REQUIRE(announcement.at("dev_ip") == agent.device_info.dev_ip);
REQUIRE(announcement.at("dev_name") == "Test printer");
REQUIRE(announcement.at("dev_name") == "Test model");
}
TEST_CASE("Stopping the status stream gives a silent printer no more than its timeouts", "[MoonrakerPrinterAgent][StatusStream]")
+59 -8
View File
@@ -8,6 +8,7 @@
#include <boost/asio/ip/address.hpp>
#include <boost/asio/ip/tcp.hpp>
#include <boost/asio/read_until.hpp>
#include <boost/asio/steady_timer.hpp>
#include <boost/asio/streambuf.hpp>
#include <boost/asio/write.hpp>
#include <boost/filesystem.hpp>
@@ -91,7 +92,9 @@ using boost::asio::ip::tcp;
class LoopbackServer
{
public:
enum class Mode { Ok, Unavailable, Silent };
// Session answers every request with the body given to set_session_body(), as the token endpoint
// does, after 200 ms so the client is still waiting when the test makes its next call.
enum class Mode { Ok, Unavailable, Silent, Session };
struct Request
{
@@ -113,6 +116,11 @@ public:
std::string url() const { return "http://127.0.0.1:" + std::to_string(m_acceptor.local_endpoint().port()); }
void set_mode(Mode mode) { m_mode = mode; }
void set_session_body(std::string body)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_session_body = std::move(body);
}
bool wait_for_accepts(size_t count, std::chrono::milliseconds timeout)
{
@@ -172,13 +180,31 @@ private:
const Mode mode = m_mode;
if (mode == Mode::Silent)
return;
const std::string_view response = mode == Mode::Ok ?
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" :
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
boost::asio::async_write(connection->socket, boost::asio::buffer(response),
[connection](boost::system::error_code, size_t) {
boost::system::error_code ignored;
connection->socket.shutdown(tcp::socket::shutdown_both, ignored);
auto response = std::make_shared<std::string>();
if (mode == Mode::Session) {
std::lock_guard<std::mutex> lock(m_mutex);
*response = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: " +
std::to_string(m_session_body.size()) + "\r\nConnection: close\r\n\r\n" + m_session_body;
} else {
*response = mode == Mode::Ok ?
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 11\r\nConnection: close\r\n\r\n{\"ok\":true}" :
"HTTP/1.1 503 Service Unavailable\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
}
auto write = [connection, response] {
boost::asio::async_write(connection->socket, boost::asio::buffer(*response),
[connection, response](boost::system::error_code, size_t) {
boost::system::error_code ignored;
connection->socket.shutdown(tcp::socket::shutdown_both, ignored);
});
};
if (mode != Mode::Session) {
write();
return;
}
auto timer = std::make_shared<boost::asio::steady_timer>(m_io, 200ms);
timer->async_wait([timer, write](boost::system::error_code ec) {
if (!ec)
write();
});
});
}
@@ -192,8 +218,12 @@ private:
std::condition_variable m_cv;
std::vector<Request> m_requests;
std::vector<std::shared_ptr<Connection>> m_connections;
std::string m_session_body;
};
// A JWT whose only claim is {"exp":4102444800} (2100-01-01); the agent reads nothing else from it.
constexpr const char* FAR_FUTURE_JWT = "eyJhbGciOiJub25lIn0.eyJleHAiOjQxMDI0NDQ4MDB9.sig";
// What the agent reported through its callbacks, recorded on whichever thread delivers it so the
// assertions can run on the test thread.
struct CallbackLog
@@ -515,3 +545,24 @@ TEST_CASE_METHOD(HealthCheckFixture, "The health check does no token work", "[Or
CHECK(agent->is_user_login());
CHECK(agent->get_access_token() == "test-token");
}
TEST_CASE_METHOD(HealthCheckFixture, "A token request made while start() refreshes waits for it instead of refreshing again", "[OrcaCloudServiceAgent]")
{
// A session saved by a previous run: start() refreshes it on its own thread.
agent->persist_user_secret(nlohmann::json{{"refresh_token", "stored-refresh-token"}, {"user_id", "test-user-id"}}.dump());
const std::string access_token = FAR_FUTURE_JWT;
server.set_session_body(nlohmann::json{{"access_token", access_token},
{"refresh_token", "rotated-refresh-token"},
{"user_id", "test-user-id"}}.dump());
server.set_mode(LoopbackServer::Mode::Session);
REQUIRE(agent->start() == BAMBU_NETWORK_SUCCESS);
CHECK(agent->ensure_token_fresh("test"));
CHECK(agent->get_access_token() == access_token);
size_t token_posts = 0;
for (const auto& request : server.requests())
if (boost::starts_with(request.request_line, "POST /auth/v1/token"))
++token_posts;
CHECK(token_posts == 1);
}
-804
View File
@@ -1,48 +1,22 @@
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/system/error_code.hpp>
#include <catch2/catch_all.hpp>
#include <libslic3r/Utils.hpp>
#include <libslic3r/libslic3r.h> // GCODEVIEWER_APP_KEY, SLIC3R_APP_KEY (via libslic3r_version.h)
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include <slic3r/plugin/PythonPluginBridge.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp> // secret_constants::USER_SECRET_FILENAME
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "libslic3r_version.h"
#include "plugin_test_utils.hpp"
#include "test_utils.hpp"
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <pybind11/cast.h>
#include <pybind11/embed.h>
#include <pybind11/eval.h>
#include <pybind11/gil.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <pybind11/stl.h>
#include <fstream>
#include <functional>
#include <ios>
#include <memory>
#include <string>
#include <utility>
#include <vector>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace py = pybind11;
namespace {
@@ -375,781 +349,3 @@ TEST_CASE("Plugin audit a scoped root can also be registered read-only", "[audit
CHECK_FALSE(mgr.check_open((readonly_dir / "vendor.json").string(), "w").allowed);
}
}
// ---------------------------------------------------------------------------
// The audit hook, run in the embedded interpreter
// ---------------------------------------------------------------------------
//
// Each TEST_CASE runs in its own process (catch_discover_tests), so each brings the plugin system
// up with the audit hook installed, once, and shuts it down at the end. The tests loop over their
// cases instead of using SECTIONs, which would restart the interpreter for every section.
namespace {
// Discovered, never loaded: the hook only needs the plugin's root and its sidecar.
const char* const AUDIT_PLUGIN_SOURCE = R"PY(# /// script
# requires-python = ">=3.12"
#
# [tool.orcaslicer.plugin]
# name = "Audit Plugin"
# description = "Plugin audit hook fixture"
# author = "OrcaSlicer"
# version = "1.0"
# type = "script"
# ///
)PY";
// A third-party library, installed outside the stdlib and outside the plugin.
const char* const LIBRARY_SOURCE = R"PY(
import socket
import urllib.request
def read_all(paths):
for path in paths:
with open(path) as f:
f.read()
def connect_from_host(port):
socket.gethostname()
socket.create_connection(("127.0.0.1", port)).close()
def fetch(url):
with urllib.request.urlopen(url) as response:
return response.read()
def fetch_each(urls):
for url in urls:
yield fetch(url)
class ReadingHandler(urllib.request.BaseHandler):
"""Reads a file while each response is processed."""
def __init__(self, path):
self.path = path
self.outcome = None
def http_response(self, request, response):
try:
open(self.path).close()
self.outcome = "ok"
except PermissionError:
self.outcome = "denied"
return response
)PY";
// Test-side Python, run outside any plugin context: files outside every allowed root, a local
// HTTP server, the library, and every module the tests use imported up front, so that no import
// happens inside a plugin context. Code from exec() runs as "<string>", which is not stdlib, so
// attempt() and fetch() stand in for plugin code.
const char* const AUDIT_SETUP = R"PY(
import http.client, http.server, os, pathlib, shutil, socket, subprocess, sys, tempfile, threading
import urllib.request, zipfile
if os.name != "nt":
import pty
APPROVED = os.path.join(OUTSIDE, "approved.txt")
PRIVATE = os.path.join(OUTSIDE, "private.txt")
MISSING = os.path.join(OUTSIDE, "missing")
TREE = os.path.join(OUTSIDE, "tree")
TRUE_COMMAND = ["cmd", "/c", "exit 0"] if os.name == "nt" else ["true"]
for path in (APPROVED, PRIVATE):
with open(path, "w") as f:
f.write("x")
for sub in ("one", "two"):
os.makedirs(os.path.join(TREE, sub))
with open(os.path.join(TREE, sub, "f.txt"), "w") as f:
f.write("x")
LIBRARY = os.path.join(OUTSIDE, "library")
os.makedirs(LIBRARY)
with open(os.path.join(LIBRARY, "netlib.py"), "w") as f:
f.write(LIBRARY_SOURCE)
sys.path.insert(0, LIBRARY)
import netlib
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/redirect":
self.send_response(302)
self.send_header("Location", URL_B)
body = b""
else:
self.send_response(200)
body = b"ok"
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
PORT = server.server_address[1]
BASE = "http://127.0.0.1:%d" % PORT
URL_A, URL_B, URL_REDIRECT = BASE + "/a", BASE + "/b", BASE + "/redirect"
urllib.request.install_opener(urllib.request.build_opener(urllib.request.ProxyHandler({})))
def fetch(url):
with urllib.request.urlopen(url) as response:
return response.read()
fetch(URL_A) # urllib imports some modules on its first request
def attempt(action):
try:
action()
return "ok"
except PermissionError:
return "denied"
def spawn_checked(argv):
parent = os.getpid()
try:
return pty.spawn(argv)
finally:
if os.getpid() != parent:
os._exit(1) # a child whose exec was refused must not go on running the tests
finalizer_outcomes = []
class TrackedRequest(urllib.request.Request):
def __del__(self):
finalizer_outcomes.append(attempt(lambda: open(PRIVATE).close()))
)PY";
struct Prompt
{
std::string plugin;
std::string event;
std::vector<std::string> targets;
};
// One line per prompt, "event: target ...", so a failed check shows every prompt.
std::string describe(const std::vector<Prompt>& prompts)
{
std::string text;
for (const Prompt& prompt : prompts) {
text += prompt.event + ":";
for (const std::string& target : prompt.targets)
text += " " + target;
text += "\n";
}
return text;
}
// The plugin system with the audit hook installed, and the permission dialog replaced by a
// recorder that answers with `approve`. Declare it first in a test, so it is destroyed last.
struct AuditHarness
{
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log.
// data_dir() is a global allowed root from then on, so the test files live in `outside`.
ScopedDataDir data{"plugin-audit-hook"};
bool initialized = PluginManager::instance().initialize();
ScopedTemporaryDir outside{"orca-plugin-audit-outside"};
std::vector<Prompt> prompts;
std::function<bool(const Prompt&)> approve = [](const Prompt&) { return false; };
AuditHarness()
{
PluginAuditManager::set_permission_prompt_for_testing(
[this](const std::string& event, const std::vector<std::string>& targets) {
prompts.push_back({PluginAuditManager::instance().current_plugin(), event, targets});
return approve(prompts.back());
});
}
~AuditHarness()
{
PluginAuditManager::set_permission_prompt_for_testing(nullptr);
PluginManager::instance().shutdown();
PythonInterpreter::instance().shutdown();
}
// Call before taking the GIL: discovery is not Python work.
void add_plugin(const std::string& key)
{
const fs::path root = data.plugins_dir() / key;
fs::create_directories(root);
std::ofstream((root / (key + ".py")).string(), std::ios::binary) << AUDIT_PLUGIN_SOURCE;
save_permissions(key, {});
PluginManager::instance().discover_plugins(/*async=*/false, /*clear=*/true);
}
// Grants as if saved in an earlier session.
void save_permissions(const std::string& key, const PluginPermissions& permissions)
{
PluginInstallState state;
state.installed_from = "local";
state.installed_version = "1.0";
state.plugin_name = key;
state.permissions = permissions;
REQUIRE(write_install_state(data.plugins_dir() / key, state));
}
PluginPermissions saved_permissions(const std::string& key) const
{
PluginInstallState state;
read_install_state(data.plugins_dir() / key, state);
return state.permissions;
}
};
// The test-side Python globals. Construct and destroy it with the GIL held; it stops the HTTP
// server before the interpreter shuts down.
struct PythonSide
{
py::dict globals;
explicit PythonSide(const AuditHarness& harness)
{
globals["OUTSIDE"] = harness.outside.path().string();
globals["LIBRARY_SOURCE"] = LIBRARY_SOURCE;
exec(AUDIT_SETUP);
}
// A Python error fails the test here, as text: an error_already_set left to propagate would
// outlive the interpreter the harness shuts down.
void exec(const char* code)
{
std::string error;
try {
py::exec(code, globals);
} catch (py::error_already_set& raised) {
error = raised.what();
}
if (!error.empty())
FAIL(error);
}
~PythonSide()
{
try {
globals["server"].attr("shutdown")();
globals["server"].attr("server_close")();
} catch (...) {
}
}
std::string str(const char* name) const { return py::str(globals[name]).cast<std::string>(); }
std::string eval(const char* expression) const { return py::str(py::eval(expression, globals)).cast<std::string>(); }
std::vector<std::string> strings(const char* name) const { return globals[name].cast<std::vector<std::string>>(); }
// Runs `code` as `plugin_key`, as a trampoline call does: inside the caller's GIL, in an
// audit scope of its own.
void run_as(const std::string& plugin_key, const char* code)
{
ScopedPluginAuditContext audit(plugin_key);
exec(code);
}
};
PluginPermissions http_grants(std::vector<std::string> urls)
{
PluginPermissions permissions;
permissions.network_http = std::move(urls);
return permissions;
}
const char* const PLUGIN = "AuditPlugin";
} // namespace
TEST_CASE("A saved request grant covers the request's own connection in a later session", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
harness.save_permissions(PLUGIN, http_grants({python.str("URL_A")}));
python.run_as(PLUGIN, "outcome = attempt(lambda: fetch(URL_A))");
CHECK(python.str("outcome") == "ok");
CHECK(describe(harness.prompts).empty());
}
TEST_CASE("A saved request grant does not cover another URL or a redirect", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
harness.save_permissions(PLUGIN, http_grants({python.str("URL_A"), python.str("URL_REDIRECT")}));
python.run_as(PLUGIN, R"PY(
outcomes = [attempt(lambda: fetch(url)) for url in (URL_A, URL_B, URL_REDIRECT)]
outcomes += [attempt(lambda: netlib.fetch(url)) for url in (URL_A, URL_B)]
outcomes.append(attempt(lambda: list(netlib.fetch_each([URL_A, URL_B]))))
)PY");
// The redirect target is asked for inside the approved redirecting request, and a library's
// wrapper or generator asks for the second URL as plain code does.
CHECK(python.strings("outcomes") == std::vector<std::string>{"ok", "denied", "denied", "ok", "denied", "denied"});
const std::string url_b = "urllib.Request: " + python.str("URL_B") + "\n";
CHECK(describe(harness.prompts) == url_b + url_b + url_b + url_b);
}
TEST_CASE("Code an approved operation calls back into is judged on its own", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
python.exec(R"PY(
CALLBACK_COPY = os.path.join(OUTSIDE, "callback-copy")
handler = netlib.ReadingHandler(PRIVATE)
handler_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), handler)
walk_outcomes = []
copy_outcomes = []
def on_walk_error(error):
walk_outcomes.append(attempt(lambda: open(PRIVATE).close()))
def copy_file(source, destination):
copy_outcomes.append(attempt(lambda: open(PRIVATE).close()))
)PY");
PluginPermissions permissions = http_grants({python.str("URL_A")});
permissions.fs_read = {python.str("APPROVED"), python.str("MISSING")};
permissions.fs_readwrite = {python.str("TREE"), python.str("CALLBACK_COPY")};
harness.save_permissions(PLUGIN, permissions);
// A library handler that urllib calls while the approved request runs, the plugin's own
// os.walk error callback and copytree copy function (called once per file), and a library
// loop over a saved file and an unsaved one.
python.run_as(PLUGIN, R"PY(
request_outcome = attempt(lambda: handler_opener.open(URL_A).read())
for _ in os.walk(MISSING, onerror=on_walk_error):
pass
shutil.copytree(TREE, CALLBACK_COPY, copy_function=copy_file)
loop_outcome = attempt(lambda: netlib.read_all([APPROVED, PRIVATE]))
)PY");
CHECK(python.str("request_outcome") == "ok");
CHECK(python.eval("handler.outcome") == "denied");
CHECK(python.strings("walk_outcomes") == std::vector<std::string>{"denied"});
CHECK(python.strings("copy_outcomes") == std::vector<std::string>{"denied", "denied"});
CHECK(python.str("loop_outcome") == "denied");
const std::string private_file = "open: " + python.str("PRIVATE") + "\n";
CHECK(describe(harness.prompts) == private_file + private_file + private_file + private_file + private_file);
}
TEST_CASE("A saved directory walk covers each directory it lists", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
PluginPermissions permissions;
permissions.fs_read = {python.str("TREE"), python.str("APPROVED")};
harness.save_permissions(PLUGIN, permissions);
// The walk lists the tree's two subdirectories on later resumes, after the plugin's own
// audited reads in between. Abandoned walks leave frames whose generator is gone, which the
// next audited event lets go.
python.run_as(PLUGIN, R"PY(
walked = []
for root, dirs, files in os.walk(TREE):
walked.append(root)
open(APPROVED).close()
globbed = list(pathlib.Path(TREE).glob("**/*"))
abandoned = os.walk(TREE)
next(abandoned)
del abandoned
open(APPROVED).close()
for _ in pathlib.Path(TREE).glob("*"):
break
open(APPROVED).close()
)PY");
CHECK(py::len(python.globals["walked"]) == 3);
CHECK(py::len(python.globals["globbed"]) == 4);
CHECK(describe(harness.prompts).empty());
}
TEST_CASE("A freshly approved operation covers its nested events", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
struct Case
{
const char* prompts;
const char* code;
};
// Each operation names targets no earlier one saved, so each is asked once. FsCreate and
// FsDelete grants are never saved, so those operations are asked every time. Only the
// expected prompts are approved, so a nested event left uncovered fails the operation.
std::vector<Case> cases = {
{"shutil.copytree", "shutil.copytree(TREE, os.path.join(OUTSIDE, 'copy'))"},
{"shutil.copyfile", "shutil.copyfile(APPROVED, os.path.join(OUTSIDE, 'copied.txt'))"},
{"shutil.move", "shutil.move(os.path.join(OUTSIDE, 'copied.txt'), os.path.join(OUTSIDE, 'moved.txt'))"},
{"shutil.copystat", "shutil.copystat(os.path.join(TREE, 'one', 'f.txt'), PRIVATE)"},
{"shutil.make_archive", "shutil.make_archive(os.path.join(OUTSIDE, 'archive'), 'zip', root_dir=TREE)"},
{"shutil.unpack_archive", "shutil.unpack_archive(os.path.join(OUTSIDE, 'archive.zip'), os.path.join(OUTSIDE, 'unpacked'))"},
{"pathlib.Path.glob", "list(pathlib.Path(TREE).glob('**/*'))"},
{"subprocess.Popen", "subprocess.run(TRUE_COMMAND, check=True)"},
{"tempfile.mkdtemp", "tempfile.mkdtemp(dir=OUTSIDE)"},
{"tempfile.mkstemp", "os.close(tempfile.mkstemp(dir=OUTSIDE)[0])"},
{"shutil.rmtree", "shutil.rmtree(os.path.join(OUTSIDE, 'copy'))"},
// send() runs after connect() has returned, so its own grant is asked once.
{"http.client.connect\nhttp.client.send",
"connection = http.client.HTTPConnection('127.0.0.1', PORT)\n"
"connection.request('GET', '/a')\n"
"connection.getresponse().read()\n"
"connection.close()"},
};
#ifndef _WIN32
cases.push_back({"os.fwalk", "list(os.fwalk(os.path.join(OUTSIDE, 'unpacked')))"});
// The forked child's exec is covered by the spawn too: had it prompted, the child's own
// denial would end it with status 1.
cases.push_back({"pty.spawn", "spawn_status = spawn_checked(TRUE_COMMAND + ['spawned'])"});
#endif
for (const Case& operation : cases) {
INFO(operation.code);
const std::string expected = std::string(operation.prompts) + "\n";
harness.approve = [&expected](const Prompt& prompt) {
return expected.find(prompt.event + "\n") != std::string::npos;
};
harness.prompts.clear();
python.run_as(PLUGIN, operation.code);
std::string asked;
for (const Prompt& prompt : harness.prompts)
asked += (asked.empty() ? "" : "\n") + prompt.event;
CHECK(asked == operation.prompts);
}
#ifndef _WIN32
CHECK(python.eval("spawn_status") == "0");
#endif
}
TEST_CASE("An event with no target is granted by name and remembered", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
harness.approve = [](const Prompt& prompt) { return prompt.event != "socket.sendto"; };
py::gil_scoped_acquire gil;
PythonSide python(harness);
python.run_as(PLUGIN, R"PY(
socket.socket().close()
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sendto_outcome = attempt(lambda: udp.sendto(b"x", ("127.0.0.1", 9)))
udp.close()
)PY");
// The socket is asked for once, without a target to show; the datagram's address is a target.
CHECK(python.str("sendto_outcome") == "denied");
CHECK(describe(harness.prompts) == "socket.__new__:\nsocket.sendto: ('127.0.0.1', 9)\n");
CHECK(harness.saved_permissions(PLUGIN).events == std::vector<std::string>{"Socket:socket.__new__"});
CHECK(harness.saved_permissions(PLUGIN).network_socket.empty());
harness.prompts.clear();
python.run_as(PLUGIN, "socket.socket().close()");
CHECK(describe(harness.prompts).empty());
// A grant for an event with no target covers nothing else: a library's approved
// gethostname() does not carry the connection it makes next.
harness.approve = [](const Prompt& prompt) { return prompt.event == "socket.gethostname"; };
python.run_as(PLUGIN, "connect_outcome = attempt(lambda: netlib.connect_from_host(PORT))");
CHECK(python.str("connect_outcome") == "denied");
CHECK(describe(harness.prompts) == "socket.gethostname:\nsocket.getaddrinfo: 127.0.0.1\n");
harness.prompts.clear();
harness.approve = [](const Prompt&) { return true; };
#ifndef _WIN32
// sendmsg() without an address names no target either.
python.run_as(PLUGIN, R"PY(
left, right = socket.socketpair()
left.sendmsg([b"x"])
left.close()
right.close()
)PY");
CHECK(describe(harness.prompts) == "socket.sendmsg:\n");
CHECK(harness.saved_permissions(PLUGIN).events ==
std::vector<std::string>{"Socket:socket.__new__", "Socket:socket.gethostname", "Socket:socket.sendmsg"});
#endif
}
TEST_CASE("An event grant does not stand in for another access or a target of the same name", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
harness.approve = [asked = 0](const Prompt&) mutable { return asked++ == 0; };
py::gil_scoped_acquire gil;
PythonSide python(harness);
python.exec(R"PY(
READ_FD = os.open(APPROVED, os.O_RDONLY)
WRITE_FD = os.open(os.path.join(OUTSIDE, "scratch.txt"), os.O_WRONLY | os.O_CREAT)
)PY");
// A file descriptor has no path to show or save: a granted read of one is not a write.
python.run_as(PLUGIN, R"PY(
fd_outcomes = [attempt(lambda: open(READ_FD, "r", closefd=False).close()),
attempt(lambda: open(WRITE_FD, "w", closefd=False).close())]
)PY");
CHECK(python.strings("fd_outcomes") == std::vector<std::string>{"ok", "denied"});
CHECK(describe(harness.prompts) == "open:\nopen:\n");
CHECK(harness.saved_permissions(PLUGIN).events == std::vector<std::string>{"FsRead:open"});
// A saved os.fork event grant is not an os.system command named "os.fork".
PluginPermissions permissions;
permissions.events = {"FsRead:open", "ProcessCreate:os.fork"};
harness.save_permissions(PLUGIN, permissions);
harness.prompts.clear();
python.run_as(PLUGIN, R"PY(
reread_outcome = attempt(lambda: open(READ_FD, "r", closefd=False).close())
system_outcome = attempt(lambda: os.system("os.fork"))
os.close(READ_FD)
os.close(WRITE_FD)
)PY");
CHECK(python.str("reread_outcome") == "ok");
CHECK(python.str("system_outcome") == "denied");
REQUIRE(harness.prompts.size() == 1);
CHECK(harness.prompts.front().event == "os.system");
CHECK(harness.saved_permissions(PLUGIN).process.empty());
}
TEST_CASE("A finalizer run when a root frame is let go is judged as the plugin without a prompt", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
PluginPermissions permissions = http_grants({python.str("URL_A")});
permissions.fs_read = {python.str("APPROVED")};
harness.save_permissions(PLUGIN, permissions);
PluginAuditManager& mgr = PluginAuditManager::instance();
// Once the request returns, only its held frame keeps the request object alive, and the
// request's finalizer reads a file the plugin was never granted.
python.run_as(PLUGIN, "fetch(TrackedRequest(URL_A))");
CHECK(python.strings("finalizer_outcomes") == std::vector<std::string>{"denied"}); // at scope end
CHECK(describe(harness.prompts).empty());
CHECK_FALSE(mgr.audit_denial_pending());
python.exec("finalizer_outcomes.clear()");
python.run_as(PLUGIN, R"PY(
fetch(TrackedRequest(URL_A))
open(APPROVED).close() # the next event lets the returned request's frame go
outcomes_before_scope_end = list(finalizer_outcomes)
)PY");
CHECK(python.strings("outcomes_before_scope_end") == std::vector<std::string>{"denied"});
CHECK(describe(harness.prompts).empty());
// The finalizer's denial during another event's decision leaves that event's own denial intact.
python.exec("finalizer_outcomes.clear()");
python.run_as(PLUGIN, R"PY(
fetch(TrackedRequest(URL_A))
try:
os.listdir(OUTSIDE)
outer_error = ""
except PermissionError as error:
outer_error = str(error)
)PY");
CHECK(python.strings("finalizer_outcomes") == std::vector<std::string>{"denied"});
CHECK_THAT(python.str("outer_error"), Catch::Matchers::ContainsSubstring("os.listdir"));
CHECK(describe(harness.prompts) == "os.listdir: " + python.str("OUTSIDE") + "\n");
AuditViolation violation;
REQUIRE(mgr.last_violation(violation));
CHECK(violation.event_name == "os.listdir");
CHECK(mgr.audit_denial_pending());
}
TEST_CASE("A nested audit scope does not see the outer scope's running operations", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin("PluginA");
harness.add_plugin("PluginB");
py::gil_scoped_acquire gil;
PythonSide python(harness);
PluginPermissions permissions;
permissions.fs_read = {python.str("TREE")};
harness.save_permissions("PluginA", permissions);
{
// Two walks plugin A started and suspended after listing the tree itself.
ScopedPluginAuditContext plugin_a("PluginA");
python.exec("first = os.walk(TREE)\nnext(first)\nsecond = os.walk(TREE)\nnext(second)");
{
// Plugin B resuming one of them is asked for each subdirectory, as plugin B.
ScopedPluginAuditContext plugin_b("PluginB");
python.exec("listed_by_b = len(list(first))");
}
// Back in plugin A's scope, its walks are covered again.
python.exec("listed_by_a = len(list(second))");
}
CHECK(python.globals["listed_by_b"].cast<int>() == 0);
CHECK(python.globals["listed_by_a"].cast<int>() == 2);
REQUIRE(harness.prompts.size() == 2);
for (const Prompt& prompt : harness.prompts) {
CHECK(prompt.plugin == "PluginB");
CHECK(prompt.event == "os.scandir");
}
}
TEST_CASE("Root frames held when a capability call ends are let go with the GIL, as the plugin", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.approve = [](const Prompt& prompt) { return prompt.event == "os.walk"; };
py::gil_scoped_acquire gil;
PythonSide python(harness);
(void) PythonPluginBridge::instance(); // force the embedded module registration into the binary
python.globals["orca"] = py::module_::import("orca");
// walk_and_drop() leaves a returned walk's frame held by the audit scope alone, with a marker
// in its locals; the frame is let go when the capability call ends, which runs the marker's
// finalizer.
python.exec(R"PY(
class Marker:
def __init__(self, action=None):
self.action = action
def __del__(self):
finalized.append(self.action() if self.action else "ran")
finalized = []
def walk_and_drop(marker):
walker = os.walk(TREE, onerror=marker)
next(walker)
del walker
class ConfigCapability(orca.PythonPluginBase):
def get_name(self):
return "config_cap"
def get_default_config(self):
walk_and_drop(Marker())
return {"finalized_during_call": len(finalized)}
def has_config_ui(self):
walker = os.walk(TREE, onerror=Marker(lambda: attempt(lambda: open(PRIVATE).close())))
next(walker)
attempt(lambda: os.listdir(OUTSIDE)) # denied: the trampoline clears the plugin key
del walker # the walk's frame is now held by the audit scope alone
raise RuntimeError("the capability fails after a denial")
class TicketAgent(orca.printer_agent.PrinterAgentBase):
def get_name(self):
return "ticket_agent"
def request_bind_ticket(self):
walk_and_drop(Marker())
return (0, "ticket")
)PY");
py::object config_instance = python.globals["ConfigCapability"]();
auto config_cap = config_instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
config_cap->set_audit_plugin_key("GilPlugin");
config_cap->set_resolved_identity("config_cap", PluginCapabilityType::Script);
py::object agent_instance = python.globals["TicketAgent"]();
auto agent_cap = agent_instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
agent_cap->set_audit_plugin_key("GilPlugin");
agent_cap->set_resolved_identity("ticket_agent", PluginCapabilityType::PrinterConnection);
auto agent = std::dynamic_pointer_cast<IPrinterAgent>(agent_cap);
REQUIRE(agent);
// A host call arrives without the GIL; both calls take it before their audit scope.
nlohmann::json config;
int ticket_result = -1;
std::string ticket;
{
py::gil_scoped_release released;
config = config_cap->get_default_config();
ticket_result = agent->request_bind_ticket(&ticket);
}
CHECK(config == nlohmann::json{{"finalized_during_call", 0}});
CHECK(ticket_result == 0);
CHECK(ticket == "ticket");
CHECK(python.strings("finalized") == std::vector<std::string>{"ran", "ran"});
// On the trampoline's denial path the frame is let go as the plugin, though the ambient key
// was cleared: the finalizer's read is audited and denied without a prompt.
python.exec("finalized.clear()");
harness.prompts.clear();
CHECK_THROWS_AS(config_cap->has_config_ui(), py::error_already_set);
CHECK(python.strings("finalized") == std::vector<std::string>{"denied"});
CHECK(describe(harness.prompts) == "os.walk: " + python.str("TREE") + "\nos.listdir: " + python.str("OUTSIDE") + "\n");
}
TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
py::gil_scoped_acquire gil;
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
const fs::path link = harness.outside.path() / "linked-stdlib";
boost::system::error_code ec;
fs::create_directory_symlink(stdlib, link, ec);
if (ec)
SKIP("Cannot create a directory symlink here: " + ec.message());
CHECK(PluginAuditManager::is_stdlib_file((stdlib / "os.py").string()));
CHECK(PluginAuditManager::is_stdlib_file((link / "urllib" / "request.py").string()));
CHECK(PluginAuditManager::is_stdlib_file("<frozen os>"));
CHECK_FALSE(PluginAuditManager::is_stdlib_file((purelib / "requests" / "api.py").string()));
CHECK_FALSE(PluginAuditManager::is_stdlib_file((link / "site-packages" / "requests" / "api.py").string()));
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
}
TEST_CASE("A plugin may read the CPython stdlib without a prompt, but not site-packages or for writing", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
// os.py ships with every CPython installation. A site-packages path is third-party code even
// when the directory is empty: the prompt must come before the missing file is noticed.
const fs::path stdlib_file = stdlib / "os.py";
REQUIRE(fs::exists(stdlib_file));
python.globals["STDLIB_FILE"] = stdlib_file.string();
python.globals["SITE_PACKAGE_FILE"] = (purelib / "regression_probe.py").string();
python.run_as(PLUGIN, R"PY(
read_outcome = attempt(lambda: open(STDLIB_FILE).read())
write_outcome = attempt(lambda: open(STDLIB_FILE, "r+").close())
site_packages_outcome = attempt(lambda: open(SITE_PACKAGE_FILE).close())
)PY");
CHECK(python.str("read_outcome") == "ok");
CHECK(python.str("write_outcome") == "denied");
CHECK(python.str("site_packages_outcome") == "denied");
// Only the write and the site-packages read are asked about; the stdlib read is implicit.
const std::string write_prompt = "open: " + python.str("STDLIB_FILE") + "\n";
const std::string site_packages_prompt = "open: " + python.str("SITE_PACKAGE_FILE") + "\n";
CHECK(describe(harness.prompts) == write_prompt + site_packages_prompt);
}
+100
View File
@@ -0,0 +1,100 @@
#include <catch2/catch_test_macros.hpp>
#include "libslic3r/GCode/ThumbnailData.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/TexturePainting.hpp"
#include "slic3r/GUI/AVVideoDecoder.hpp"
#include "slic3r/GUI/Printer/BambuTunnel.h"
#include <atomic>
#include <cstddef>
#include <cstdlib>
#include <thread>
#include <vector>
#include <wx/bitmap.h>
#include <wx/gdicmn.h>
#include <wx/image.h>
#include <wx/init.h>
#include <wx/mstream.h>
TEST_CASE("JPEG texture decoding coexists with thumbnails and video frames", "[TimelapseCodecs][Regression]")
{
#if defined(__linux__)
if (!std::getenv("DISPLAY") && !std::getenv("WAYLAND_DISPLAY"))
SKIP("This integration test needs a display (for example, xvfb-run).");
#endif
wxInitializer init;
REQUIRE(init.IsOk());
wxInitAllImageHandlers();
// Generate a JPEG through the same encoder used for G-code thumbnails.
constexpr int width = 160, height = 90, iterations = 1000;
Slic3r::ThumbnailData thumbnail;
thumbnail.set(width, height);
for (std::size_t i = 0; i < thumbnail.pixels.size(); i += 4) {
thumbnail.pixels[i] = 180;
thumbnail.pixels[i + 1] = 70;
thumbnail.pixels[i + 2] = 30;
thumbnail.pixels[i + 3] = 255;
}
const auto jpeg = Slic3r::GCodeThumbnails::compress_thumbnail(thumbnail, Slic3r::GCodeThumbnailsFormat::JPG);
REQUIRE(jpeg);
REQUIRE(jpeg->size > 0);
const auto* bytes = static_cast<const unsigned char*>(jpeg->data);
Slic3r::TextureImage texture;
texture.width = texture.height = -1;
texture.data.assign(bytes, bytes + jpeg->size);
std::atomic<bool> texture_ok{true};
std::thread texture_worker([&] {
for (int i = 0; i < iterations; ++i) {
std::vector<unsigned char> pixels;
int decoded_width = 0, decoded_height = 0;
if (!Slic3r::decode_texture_to_pixels(texture, pixels, decoded_width, decoded_height) ||
decoded_width != width || decoded_height != height || pixels.size() != width * height * 3)
texture_ok = false;
}
});
// PrinterFileSystem decodes timelapse thumbnails with wxImage. Do not
// assert until the worker is joined: Catch assertions can unwind the scope.
bool thumbnails_ok = true;
for (int i = 0; i < iterations; ++i) {
wxMemoryInputStream stream(jpeg->data, jpeg->size);
wxImage image(stream, "image/jpeg");
if (!image.IsOk() || image.GetWidth() != width || image.GetHeight() != height ||
image.GetRed(width / 2, height / 2) < 170 || image.GetGreen(width / 2, height / 2) > 80)
thumbnails_ok = false;
}
texture_worker.join();
REQUIRE(texture_ok.load());
REQUIRE(thumbnails_ok);
// Exercise Orca's real FFmpeg decoder and its platform display conversion.
AVVideoDecoder decoder;
Bambu_StreamInfo info{};
info.type = VIDE;
info.sub_type = MJPG;
info.format_type = video_jpeg;
REQUIRE(decoder.open(info) == 0);
Bambu_Sample sample{};
sample.size = static_cast<int>(jpeg->size);
sample.buffer = bytes;
for (int i = 0; i < iterations; ++i) {
REQUIRE(decoder.decode(sample) == 0);
wxImage image;
REQUIRE(decoder.toWxImage(image, wxSize(width, height)));
REQUIRE(image.IsOk());
REQUIRE(image.GetWidth() == width);
REQUIRE(image.GetHeight() == height);
#ifdef _WIN32
// Playback uses this raw 32-bit bitmap conversion only on Windows.
wxBitmap bitmap;
REQUIRE(decoder.toWxBitmap(bitmap, wxSize(width, height)));
REQUIRE(bitmap.IsOk());
REQUIRE(bitmap.GetWidth() == width);
REQUIRE(bitmap.GetHeight() == height);
#endif
}
}