diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml
index ab4cd57f5f..b849979de0 100644
--- a/.github/workflows/build_all.yml
+++ b/.github/workflows/build_all.yml
@@ -5,7 +5,6 @@ on:
branches:
- main
- release/*
- - belt-printer
paths:
- 'deps/**'
- 'src/**'
@@ -262,9 +261,6 @@ jobs:
date:
ver:
ver_pure:
- # Belt-printer nightlies share the main nightly release but carry a `_belt`
- # suffix so they never overwrite the main assets.
- nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: "Remove unneeded stuff to free disk space"
run:
@@ -439,13 +435,13 @@ jobs:
name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
- name: Deploy Flatpak to nightly release
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer')
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
- asset_name: OrcaSlicer-Linux-flatpak_nightly${{ env.nightly_suffix }}_${{ matrix.variant.arch }}.flatpak
+ asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# The asset is /app (the exes link it at runtime) plus the build tree
diff --git a/.github/workflows/build_orca.yml b/.github/workflows/build_orca.yml
index 7b001a8c48..7115efab09 100644
--- a/.github/workflows/build_orca.yml
+++ b/.github/workflows/build_orca.yml
@@ -33,11 +33,6 @@ jobs:
ubuntu-ver: '2404'
ubuntu-ver-str: '_Ubuntu2404'
ORCA_UPDATER_SIG_KEY: ${{ secrets.ORCA_UPDATER_SIG_KEY }}
- # Branches whose builds are published to the nightly release. The
- # belt-printer branch ships alongside main but its assets carry a `_belt`
- # suffix (nightly_suffix) so they never overwrite the main nightly assets.
- deploy_nightly: ${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' }}
- nightly_suffix: ${{ github.ref == 'refs/heads/belt-printer' && '_belt' || '' }}
steps:
- name: Checkout
@@ -260,7 +255,7 @@ jobs:
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/belt-printer' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && runner.os == 'macOS' && inputs.macos-combine-only
timeout-minutes: 30
working-directory: ${{ github.workspace }}
env:
@@ -348,7 +343,7 @@ jobs:
fi
- name: Create DMG without notary
- if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/belt-printer' && runner.os == 'macOS' && inputs.macos-combine-only
+ if: github.ref != 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only
working-directory: ${{ github.workspace }}
run: |
# Load the `retry` helper (retries flaky commands such as `hdiutil create`).
@@ -394,13 +389,13 @@ jobs:
if-no-files-found: ignore
- name: Deploy Mac release
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'macOS' && inputs.macos-combine-only && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
- asset_name: OrcaSlicer_Mac_universal_nightly${{ env.nightly_suffix }}.dmg
+ asset_name: OrcaSlicer_Mac_universal_nightly.dmg
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
@@ -543,24 +538,24 @@ jobs:
path: ${{ github.workspace }}/build/src/Release/OrcaSlicer_profile_validator.exe
- name: Deploy Windows release portable
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_${{ env.ver }}${{ env.ARCH_SUFFIX }}_portable.zip
- asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}_portable.zip
+ asset_name: OrcaSlicer_Windows${{ env.ARCH_SUFFIX }}_nightly_portable.zip
asset_content_type: application/x-zip-compressed
max_releases: 1
- name: Deploy Windows release installer
- if: github.repository == 'OrcaSlicer/OrcaSlicer' && env.deploy_nightly == 'true' && runner.os == 'Windows' && !vars.SELF_HOSTED
+ if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main' && runner.os == 'Windows' && !vars.SELF_HOSTED
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ${{ github.workspace }}/${{ env.BUILD_DIR }}/OrcaSlicer_Windows_Installer_${{ env.ver }}${{ env.ARCH_SUFFIX }}.exe
- asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly${{ env.nightly_suffix }}.exe
+ asset_name: OrcaSlicer_Windows_Installer${{ env.ARCH_SUFFIX }}_nightly.exe
asset_content_type: application/x-msdownload
max_releases: 1
@@ -701,13 +696,13 @@ jobs:
path: './build/src/dev-utils/Release/generate_system_cache'
- name: Deploy Ubuntu release
- if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && env.deploy_nightly == 'true' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
+ if: ${{ github.repository == 'OrcaSlicer/OrcaSlicer' && ! env.ACT && github.ref == 'refs/heads/main' && runner.os == 'Linux' && !vars.SELF_HOSTED }}
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: ./build/OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_${{ env.ver }}.AppImage
- asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly${{ env.nightly_suffix }}.AppImage
+ asset_name: OrcaSlicer_Linux_AppImage${{ env.ubuntu-ver-str }}${{ env.arch_suffix }}_nightly.AppImage
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
- name: Deploy Ubuntu release
diff --git a/README.md b/README.md
index 40ba7637b9..408efb57b7 100644
--- a/README.md
+++ b/README.md
@@ -68,6 +68,8 @@ If you come across any of these in search results, please report them as
Regular updates fueled by continuous community contributions.
- **Wide Printer Compatibility**
Supports a broad range of printers: Bambu Lab, Prusa, Creality, Voron, and more.
+- **[Belt Printer Support](https://www.orcaslicer.com/wiki/belt_printing)**
+ Slice for belt / conveyor (infinite-Z) printers, with belt-aware supports and a tilted-bed preview. Contributed by [Joseph Robertson (@HarrierPigeon)](https://github.com/HarrierPigeon).
- Additional features can be found in the [change notes](https://github.com/OrcaSlicer/OrcaSlicer/releases/).
# Wiki
@@ -89,17 +91,6 @@ Visit our GitHub Releases page for the latest stable version of OrcaSlicer, reco
🌙 **[Download the Latest Nightly Build](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds)**
Explore the latest developments in OrcaSlicer with our nightly builds. Feedback on these versions is highly appreciated.
-### Belt Printer Builds
-
-The [nightly release](https://github.com/OrcaSlicer/OrcaSlicer/releases/tag/nightly-builds) ships **two parallel builds**: the standard build and a belt-printer build. Both are attached to the same release — tell them apart by the filename suffix:
-
-- **Standard** — no suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly.exe`)
-- **Belt** — `_belt` suffix (e.g. `OrcaSlicer_Windows_Installer_x64_nightly_belt.exe`)
-
-The `_belt` builds add **experimental support for belt / conveyor (infinite-Z) printers**, where the model is sliced against a tilted belt surface instead of a flat horizontal bed. They include ready-to-use belt printer profiles, the full belt slicing pipeline (mesh rotation and G-code transforms), belt-aware support generation, and a tilted-bed preview.
-
-> ⚠️ Belt printer support is under active development and is **not yet merged into `main`** — it currently ships only in these parallel `_belt` builds, produced from the [`belt-printer`](https://github.com/OrcaSlicer/OrcaSlicer/tree/belt-printer) branch. See tracking PR [#14394](https://github.com/OrcaSlicer/OrcaSlicer/pull/14394) and the original documentation in [#12998](https://github.com/OrcaSlicer/OrcaSlicer/pull/12998).
-
# How to install
## Windows
diff --git a/deps/OpenSSL/OpenSSL.cmake b/deps/OpenSSL/OpenSSL.cmake
index 2bbf2b81b7..35b17ed50e 100644
--- a/deps/OpenSSL/OpenSSL.cmake
+++ b/deps/OpenSSL/OpenSSL.cmake
@@ -44,6 +44,18 @@ else()
if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
else()
+ # A static library that is embedded into a shared object must not export
+ # its symbols. On Linux the running process also loads the system OpenSSL
+ # 3.x (WebKitGTK/gnutls pull in libcrypto.so.3), and CPython's _ssl and
+ # _hashlib are dlopened (RTLD_LOCAL) DSOs that each embed this OpenSSL.
+ # With default visibility their unversioned OpenSSL references are
+ # preempted by that global 3.x copy, mixing the 1.1.1 and 3.x ABIs and
+ # corrupting the heap (ssl.create_default_context() aborts). Hidden
+ # visibility makes each embedded copy self-contained. Linux-only: macOS
+ # binds dylibs with a two-level namespace (no interposition) and ships no
+ # OpenSSL, and Windows has no equivalent flag and no system OpenSSL to
+ # collide with.
+ set(_openssl_extra_cflags -fvisibility=hidden)
set(_conf_cmd env "CC=${CMAKE_C_COMPILER}" "LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}" "./config")
endif()
set(_cross_comp_prefix_line "")
@@ -102,3 +114,20 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
COMMAND ${CMAKE_COMMAND} -E copy_directory openssl "${DESTDIR}${CMAKE_INSTALL_LIBDIR}/cmake/openssl"
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
)
+
+if (NOT WIN32 AND NOT APPLE)
+ # OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
+ # example to add -fvisibility=hidden) relinks the archives from stale
+ # objects instead of recompiling them, and the change silently has no
+ # effect. Drop the objects whenever this recipe changes so the next build
+ # actually recompiles them.
+ ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
+ ExternalProject_Add_Step(dep_OpenSSL clean_objects
+ DEPENDEES configure
+ DEPENDERS build
+ COMMAND make clean
+ WORKING_DIRECTORY "${SOURCE_DIR}"
+ DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
+ COMMENT "OpenSSL: cleaning objects after a recipe change"
+ )
+endif ()
diff --git a/deps/python3/python3.cmake b/deps/python3/python3.cmake
index 6b4443ef3a..b0415f2bb5 100644
--- a/deps/python3/python3.cmake
+++ b/deps/python3/python3.cmake
@@ -299,3 +299,27 @@ endif()
if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB)
endif()
+
+if (NOT WIN32 AND NOT APPLE)
+ # CPython's Makefile rules for _ssl and _hashlib depend only on their own
+ # sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
+ # them relink and they keep the previous symbols. On an incremental tree,
+ # drop the built modules and relink them against the current OpenSSL; a
+ # fresh build is left alone (its PGO target builds them). "make" alone is a
+ # no-op once PGO has run, so sharedmods is invoked explicitly.
+ ExternalProject_Get_Property(dep_python3 SOURCE_DIR)
+ file(GLOB _python_ssl_modules
+ "${SOURCE_DIR}/Modules/_ssl*.so"
+ "${SOURCE_DIR}/Modules/_hashlib*.so")
+ if (_python_ssl_modules)
+ ExternalProject_Add_Step(dep_python3 relink_ssl_extensions
+ DEPENDEES configure
+ DEPENDERS build
+ COMMAND sh -c "rm -f '${SOURCE_DIR}'/Modules/_ssl*.so '${SOURCE_DIR}'/Modules/_hashlib*.so && make -j${NPROC} sharedmods"
+ WORKING_DIRECTORY "${SOURCE_DIR}"
+ COMMENT "CPython: relinking _ssl/_hashlib against the current OpenSSL"
+ DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
+ "${CMAKE_CURRENT_LIST_DIR}/../OpenSSL/OpenSSL.cmake"
+ )
+ endif ()
+endif ()
diff --git a/docs/HLSD/adaptive-tpms-infill.md b/docs/HLSD/adaptive-tpms-infill.md
new file mode 100644
index 0000000000..5fc578f7e2
--- /dev/null
+++ b/docs/HLSD/adaptive-tpms-infill.md
@@ -0,0 +1,221 @@
+# Adaptive TPMS infill — High Level Design
+
+## Purpose and scope
+
+`tpms_adaptive` 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
+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
+goes from one to the other. Only internal sparse infill is graded; the Gyroid
+Z-buckling optimization does not apply to it.
+
+The design has two parts: a field built once per object, and a pattern made
+from it, warped around the center of each lobe of a body so that its cell size
+follows the field, or, in the modes following the distance to the surface,
+split into shells or blended between densities.
+
+## Radial field
+
+`TpmsRadialField` 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
+`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
+field depends on the geometry only, the densities are applied per region in the
+fill. An object thinner than the grid cells has no body in the field; no field
+is kept then, and the infill falls back to the regular pattern. In the modes
+following the distance to the surface, the field also gives the depth of every
+point (see below).
+
+A regular 3D grid of cubic cells is rasterized from the `lslices` of the layers,
+so the field follows what is printed: negative volumes, the union of
+overlapping parts and holes are taken into account, and the mesh does not need
+to be closed. A padding node around the grid is always outside. The grid is
+capped at about a million nodes, with cells no smaller than 0.5 mm.
+
+- Bodies are the connected inside nodes. Each is graded on its own, so separate
+ parts of one object each get their own sparse center.
+- A body is split into lobes around the local maxima of the depth, by an exact
+ Euclidean distance transform (Felzenszwalb and Huttenlocher, one pass per
+ axis). Two maxima are in separate lobes when the depth along the segment
+ between them drops below 0.8 of the shallower one, like at the neck between
+ two united spheres; maxima shallower than 0.3 of the deepest one are ignored.
+ A maximum joins the first lobe whose first maximum it sees without a neck.
+ The lobes are made one at a time, the remaining maxima tested against the
+ first one in parallel, as a plate has a whole plane of them.
+ Where the depth ties along a line or a plane, as in a tall box, the lobe's
+ center is the node nearest to the middle of the tied nodes, so the center is
+ in the middle of the height and not a column.
+- A point belongs to the lobe it is nearest to relative to their depths, so the
+ side between two lobes is nearer to the smaller one. Near that side, within a
+ tenth of that relative distance, the patterns of the lobes morph into each
+ other, so the lines stay continuous. Every lobe in that range takes part, up
+ to four, so the morph is also continuous where three or four lobes meet.
+- The reach of a lobe is the distance from its center to the first exit along
+ 24 x 48 latitude-longitude directions, smoothed twice over neighbouring
+ directions in log space. Towards a neighbouring lobe it stops at twice the
+ distance to the side between them, so that side is graded half way, as deep
+ as a neck is, rather than as sparse as the center or as dense as the surface.
+ Only the lobes whose centers are near enough to be nearer at the current
+ distance are compared along a ray, so many lobes, as in a perforated plate,
+ stay cheap.
+ The radial coordinate of a point is its distance to the center over the reach
+ in its direction. Behind a gap, as across the hole
+ of a ring, the radial coordinate is above 1 and the infill keeps the surface
+ density.
+- Every outside node belongs to its nearest body, so points near a surface find
+ their body without a search. With a single body, all nodes belong to it.
+
+In the 2D modes, every plane of nodes normal to the axis is a field of its own:
+the distance transform skips the axis, bodies, lobes and the nearest body are
+found within the plane, and the reach is sampled on a circle of 48 directions.
+A point is looked up in the two planes around it, the weights of their lobes
+interpolated along the axis, so the grading does not step between planes; a
+plane without a body uses the nearest one that has one. The planes are a cell
+apart, not a layer: where the sections change abruptly, as at a step, the
+patterns of the two planes morph into each other over that cell.
+
+A distance to the nearest surface would be the obvious field, but no smooth map
+follows it. By the divergence theorem, the mean scale of a map over a body is
+fixed by its values on the surface: a map that keeps the full density along the
+whole surface, as the distance would ask under the top and bottom, has the mean
+density of the uniform infill, the sparser core being paid for by lines crowding
+along the walls. The layers of a plate at different depths would also need
+different line spacings in the same directions, which no continuous map allows
+without shearing across the plate. Following the distance needs changes of the
+topology of the lattice (see below). The radial coordinate instead grades what a
+single map can: towards one point.
+
+## Warped pattern
+
+The pattern is evaluated on warped coordinates:
+
+ TPMS(f_surface * m(t) * (p - center))
+
+where `m` scales the pattern around the center of the lobe: its frequency is
+`m + t * m'` along the ray and `m` across it. `m(t)` is the mean of the target
+scale over the ball of radius `t`, `3 / t^3 * integral of s^2 * target(s) ds`, so
+the mean of the three, and with it the density, follows the gradient. The cells
+are round at the center; near the surface they are flattened, with the lines
+running parallel to it. Beyond the surface the target is the surface scale, so
+the warp extends continuously outside.
+
+In the 2D modes only the coordinates within the plane are warped, and `m(t)` is
+the mean over the disc, `2 / t^2 * integral of s * target(s) ds`. Along the axis
+the pattern keeps the interior frequency: scaling it with `m` would shear the
+pattern by the distance along the axis times the gradient of `m`, without bound
+on a long object. The cells are round at the center and stretched along the
+axis near the surface. With Normal Z the layers are graded exactly, since
+the lines of a layer follow its in-plane frequencies; normal to X or Y, the
+layers near the sides are as dense as the larger of the two frequencies in the
+layer, which is the surface one.
+
+Evaluating a TPMS at a frequency that varies with the position without such a
+map distorts it wherever the frequency changes, because the phase also changes
+with the gradient of the frequency times the distance from the origin. Fitting a
+smooth map to a varying isotropic scale in the least-squares sense (a Poisson
+problem per axis) cannot grade strongly: its divergence is the target scale plus
+a harmonic function pinned by the surface, which keeps the scale in the core
+near two thirds of the surface one. Following a distance exactly needs the
+lattice to change its topology, by blending lattices of different densities or
+filling shells of equal distance with them, as the modes following the distance
+to the surface do.
+
+The target scale at depth `d = 1 - t`, with `S` the surface and `I` the interior
+frequency, both from each pattern's own density calibration:
+
+| Gradient | Scale |
+|-------------|------------------------|
+| Linear | `1 + (I / S - 1) * d` |
+| Quadratic | `1 + (I / S - 1) * d^2`|
+| Exponential | `(I / S)^d` |
+
+With a denser surface, quadratic keeps the surface density deepest and
+exponential drops fastest. A denser interior works the same way.
+
+The zero level is extracted with marching squares like the regular TPMS-FK, on
+a sampling grid fixed in the fill frame like the optimized Gyroid, so that every
+region of a layer connects its lines the same way at the saddles of the pattern.
+Loops narrower than two lines (shorter than `2 * PI * spacing`) are dropped, as
+they would print as blobs. The fill works in a frame rotated by the infill
+angle, so the radial field is looked up at the point rotated back into the
+object frame, and the center rotated into the fill frame. Both use the middle of
+the layer.
+
+## Modes following the distance to the surface
+
+`distance_warp`, `smooth_blend` and `stepped_shells` grade by the distance to
+the nearest surface, including the top and bottom, as concentric shells do. The
+depth of a point is that distance over the distance of the deepest point of its
+body, from 0 at the surface to 1; the field keeps it for every node and
+interpolates it between them. A tall box so keeps its whole axis as sparse as
+its center, and a plate is graded through its thickness.
+
+No single smooth pattern follows that depth without distortion (see above), so
+the three modes trade differently:
+
+- Distance warp keeps the lobes and the warp of Lobes, but its radial profile
+ comes from the depth. For every direction of a lobe, the mean depth over the
+ ball along the ray is sampled at 33 radii up to the reach, then smoothed over
+ the neighbouring directions like the reach. The radial coordinate is the one
+ of the linear profile with the same mean depth, `t = 4/3 * (1 - mean depth)`,
+ so with a linear gradient the mean cell size follows the depth exactly, and
+ with the others approximately. In a sphere or a cube, where the depth falls
+ linearly along every ray, it is Lobes. Elsewhere the profile changes with the
+ direction, and the warp shears where neighbouring directions differ, as in
+ plates and long bodies; right under the top of a long body the cells are
+ sparser within the layer, the warp moving their density into the height.
+ The profiles are smoothed over the directions like the reach, as sharper
+ ones shear the pattern across the layer, which adds lines. A long body is so
+ graded partly along its length, between Lobes and the distance.
+- Smooth blend evaluates the regular patterns of the two levels around the
+ target of every point and blends them by a smoothstep over the whole gap
+ between the levels, here at most 2.5 times apart. The density follows the
+ depth without steps, but where two lattices blend, part of their lines run
+ along the blend, so fewer levels print fewer extra lines. From 25% to 5%,
+ three levels print about 0.45 of the uniform infill in a deep core whose
+ levels alone would print 0.3; levels 1.5 times apart print 0.6 to 0.7, and a
+ single blend from the surface to the interior 0.6.
+- Stepped shells split each region of a layer into shells and fill every shell
+ with the regular pattern at its density. The densities are levels from the
+ surface to the interior density at most 1.5 times apart, five from 25% to 5%,
+ and a point takes the level nearest to its target on that geometric scale.
+ The shells are traced by marching squares of the continuous level over the
+ layer on a 0.5 mm grid fixed in the object, so every region of a layer gets
+ the same shells, and clipped to the region. Each shell is shrunk by half a
+ line, like a filled region, and its regular filler connects its lines along
+ that boundary, so the connections of two neighbouring shells lie side by side
+ instead of on top of each other. The pattern is never
+ distorted, but its lines end at every shell, and thin parts get thin shells.
+ The connections add lines: in the core of a 60 mm cube, about a third more
+ than the target.
+
+Stepped shells and Smooth blend need neither lobes nor reaches, which are not
+built for them.
+
+## Constraints
+
+- With `tpms_adaptive` 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.
+- At a sparse infill density of 100% the sparse infill is turned into solid
+ infill, so there is nothing to grade: the options are hidden and no field is
+ built.
+- The adaptive options invalidate `posPrepareInfill`, which rebuilds the field
+ and the anchoring infill.
+- An elongated body without a neck has one center, so its far ends are graded
+ as the outer part of the body, and the warp shears where the reach changes
+ quickly with the direction. A concave body, like an L, may be split into lobes
+ where its maxima cannot see each other in a straight line.
+- Across a ray, the scale is the mean of the gradient from the center, so the
+ layers right under the top and above the bottom are sparser than the surface
+ density in their middle, and a plate is graded from its middle outwards rather
+ than through its thickness.
diff --git a/resources/images/delete_all_filaments.svg b/resources/images/delete_all_filaments.svg
new file mode 100644
index 0000000000..e2f3641300
--- /dev/null
+++ b/resources/images/delete_all_filaments.svg
@@ -0,0 +1 @@
+
diff --git a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json
index 8ca40003f3..44bd17e125 100644
--- a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json
+++ b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S 0.2 nozzle.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000"
diff --git a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json
index bd399e65a1..1867783ab5 100644
--- a/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.08mm High Quality @BBL P2S.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json
index 02ec0007ec..2d00b3d04e 100644
--- a/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json
+++ b/resources/profiles/BBL/process/0.10mm Standard @BBL P2S 0.2 nozzle.json
@@ -9,6 +9,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000"
diff --git a/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json b/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json
index e3471e88b0..37194e5627 100644
--- a/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json
+++ b/resources/profiles/BBL/process/0.12mm Balanced Quality @BBL P2S 0.2 nozzle.json
@@ -9,6 +9,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000"
diff --git a/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json
index a9c9eb7bdf..2847f1cae2 100644
--- a/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.12mm High Quality @BBL P2S.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json
index 4d6e809633..cdd946b32f 100644
--- a/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.16mm High Quality @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json
index 657b0b7c38..41bc2ba0f0 100644
--- a/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.16mm Standard @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json
index 4d2722df04..2103b9cc04 100644
--- a/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json
+++ b/resources/profiles/BBL/process/0.18mm Balanced Quality @BBL P2S 0.6 nozzle.json
@@ -11,6 +11,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json b/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json
index 2a76fcba00..46a148408c 100644
--- a/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.20mm High Quality @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"4000",
"4000",
diff --git a/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json
index 13c9f1e722..54ec7a734a 100644
--- a/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.20mm Standard @BBL P2S.json
@@ -11,6 +11,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json
index 24f35fc875..6ca9a556a9 100644
--- a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json
+++ b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.6 nozzle.json
@@ -11,6 +11,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json
index 64c46e6414..47bcce8c51 100644
--- a/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json
+++ b/resources/profiles/BBL/process/0.24mm Balanced Quality @BBL P2S 0.8 nozzle.json
@@ -10,6 +10,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000"
diff --git a/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json b/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json
index bec3147a16..90ef61fe8c 100644
--- a/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json
+++ b/resources/profiles/BBL/process/0.24mm Standard @BBL P2S.json
@@ -10,6 +10,7 @@
"50",
"50"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json b/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json
index d745625988..22171f7b4f 100644
--- a/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json
+++ b/resources/profiles/BBL/process/0.30mm Standard @BBL P2S 0.6 nozzle.json
@@ -11,6 +11,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000",
diff --git a/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json
index 979edcca6e..b6f59a2ce1 100644
--- a/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json
+++ b/resources/profiles/BBL/process/0.32mm Balanced Quality @BBL P2S 0.8 nozzle.json
@@ -10,6 +10,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000"
diff --git a/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json b/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json
index d3aaa84887..843a06b9a9 100644
--- a/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json
+++ b/resources/profiles/BBL/process/0.40mm Standard @BBL P2S 0.8 nozzle.json
@@ -10,6 +10,7 @@
"30",
"30"
],
+ "enable_arc_fitting": "0",
"default_acceleration": [
"10000",
"10000"
diff --git a/resources/shaders/110/texture_displacement_shaded.fs b/resources/shaders/110/texture_displacement_shaded.fs
index 4a2980cd64..44ea89b844 100644
--- a/resources/shaders/110/texture_displacement_shaded.fs
+++ b/resources/shaders/110/texture_displacement_shaded.fs
@@ -29,19 +29,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
-// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b,
-// num parts of a in every den, and the print shows that interleave rather than the entry's average
-// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does
-// per triangle on the CPU, so the preview shows the pattern the bake will print.
+// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
+// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
+// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
-uniform int palette_num[64];
-uniform int palette_den[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
-uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
-uniform float layer_height; // mm; one Z band per print layer
-uniform float dither_cell; // mm; one XY dither cell
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -229,63 +223,16 @@ int nearest_palette_entry(vec3 rgb)
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
-float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
-// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
-// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
-// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
-// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
-// integer % (not available on every GLSL 1.10 target).
-vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
+// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
+// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
+// per print layer, so there is nothing left to interleave here.
+vec3 printed_color(int index)
{
int a = palette_a[index];
- int b = palette_b[index];
- if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
+ if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
- if (a == b)
- return filament_rgb[a];
- float num = float(palette_num[index]);
- float den = float(palette_den[index]);
- // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
- if (mix_mode == 2 && abs(normal.z) >= 0.7)
- return filament_rgb[(num * 2.0 >= den) ? a : b];
-
- // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
- // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
- // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
- // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
- // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
- // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
- // pattern is finer than this view can resolve, show what the print looks like from here, which is
- // the entry's perceptual average. The Normal view remains where the per-facet truth lives.
- float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
- float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
- float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
- if (sharp <= 0.0)
- return palette_rgb[index];
-
- vec3 picked;
- if (mix_mode == 1) {
- // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
- // 0 8 2 10
- // 12 4 14 6
- // 3 11 1 9
- // 15 7 13 5
- // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
- // no constant arrays).
- float cell = max(dither_cell, 0.01);
- float gx = mod(floor(pos.x / cell), 4.0);
- float gy = mod(floor(pos.y / cell), 4.0);
- float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
- picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
- } else {
- // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
- // operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
- float slot = floor(pos.z / max(layer_height, 0.01));
- float phase = mod(slot, den);
- picked = filament_rgb[(phase < num - 0.5) ? a : b];
- }
- return mix(palette_rgb[index], picked, sharp);
+ return filament_rgb[a];
}
void main()
@@ -296,9 +243,6 @@ void main()
// World millimetres throughout, like the bake - see the 140 variant.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
- // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
- // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
- vec3 pos_fwidth = fwidth(world_pos.xyz);
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -420,6 +364,6 @@ void main()
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
- albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
+ albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
diff --git a/resources/shaders/140/texture_displacement_shaded.fs b/resources/shaders/140/texture_displacement_shaded.fs
index 1d8bee10fa..b2936598cd 100644
--- a/resources/shaders/140/texture_displacement_shaded.fs
+++ b/resources/shaders/140/texture_displacement_shaded.fs
@@ -88,19 +88,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
-// How each entry prints. A pure entry is one filament (a == b); a mix interleaves filaments a and b,
-// num parts of a in every den, and the print shows that interleave rather than the entry's average
-// colour. The fragment resolves it exactly as GLGizmoTextureDisplacement::make_mix_resolver() does
-// per triangle on the CPU, so the preview shows the pattern the bake will print.
+// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
+// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
+// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
-uniform int palette_num[64];
-uniform int palette_den[64];
uniform vec3 filament_rgb[16];
uniform int filament_count;
-uniform int mix_mode; // ColorMixMode: 0 Z bands, 1 XY dither, 2 auto
-uniform float layer_height; // mm; one Z band per print layer
-uniform float dither_cell; // mm; one XY dither cell
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -295,63 +289,16 @@ int nearest_palette_entry(vec3 rgb)
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
-float bayer2(float x, float y) { return 2.0 * x + 3.0 * y - 4.0 * x * y; }
-// The colour the printer lays down at world point `pos` for palette entry `index`: its filament, or
-// for a mix whichever of its two filaments this point falls on. Mirrors make_mix_resolver() on the
-// CPU, floors on the band/cell size included. All the modular arithmetic is done in floats with
-// mod(), which wraps negative coordinates the way the CPU's ((v % n) + n) % n does and needs no
-// integer % (not available on every GLSL 1.10 target).
-vec3 printed_color(int index, vec3 pos, vec3 normal, vec3 footprint)
+// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
+// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
+// per print layer, so there is nothing left to interleave here.
+vec3 printed_color(int index)
{
int a = palette_a[index];
- int b = palette_b[index];
- if (a < 0 || a >= filament_count || b < 0 || b >= filament_count)
+ if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
- if (a == b)
- return filament_rgb[a];
- float num = float(palette_num[index]);
- float den = float(palette_den[index]);
- // Auto: bands where the surface is steeper than ~45 degrees, the dominant filament elsewhere.
- if (mix_mode == 2 && abs(normal.z) >= 0.7)
- return filament_rgb[(num * 2.0 >= den) ? a : b];
-
- // Pre-filter. The interleave is an ordered dither the eye is meant to blend away, and no dither
- // blends when it is drawn at less than a few pixels per period - it aliases, which is what turned
- // every upright wall into horizontal streaks: the Z band cycle is den * layer_height (around a
- // millimetre), and every pixel of a row on a vertical wall shares one z, so each row came out as a
- // 1-bit threshold of the image at that row's phase. `footprint` is mm of world position per pixel,
- // so this is zoom- and resolution-correct rather than a tuned constant: where the print's own
- // pattern is finer than this view can resolve, show what the print looks like from here, which is
- // the entry's perceptual average. The Normal view remains where the per-facet truth lives.
- float period = (mix_mode == 1) ? 2.0 * max(dither_cell, 0.01) : den * max(layer_height, 0.01);
- float px = (mix_mode == 1) ? max(footprint.x, footprint.y) : footprint.z;
- float sharp = clamp(period / max(4.0 * px, 1e-6) - 0.5, 0.0, 1.0);
- if (sharp <= 0.0)
- return palette_rgb[index];
-
- vec3 picked;
- if (mix_mode == 1) {
- // Ordered 4x4 Bayer over floor(x / cell), floor(y / cell). The CPU's table
- // 0 8 2 10
- // 12 4 14 6
- // 3 11 1 9
- // 15 7 13 5
- // is 4 * bayer2(x % 2, y % 2) + bayer2(x / 2, y / 2), which needs no array (GLSL 1.10 has
- // no constant arrays).
- float cell = max(dither_cell, 0.01);
- float gx = mod(floor(pos.x / cell), 4.0);
- float gy = mod(floor(pos.y / cell), 4.0);
- float bayer = 4.0 * bayer2(mod(gx, 2.0), mod(gy, 2.0)) + bayer2(floor(gx / 2.0), floor(gy / 2.0));
- picked = filament_rgb[(num / den > (bayer + 0.5) / 16.0) ? a : b];
- } else {
- // Z bands: one per band height, the band's phase in the a/b cycle picks the filament. Both
- // operands are integer-valued, so the half keeps "phase < num" exact under float rounding.
- float slot = floor(pos.z / max(layer_height, 0.01));
- float phase = mod(slot, den);
- picked = filament_rgb[(phase < num - 0.5) ? a : b];
- }
- return mix(palette_rgb[index], picked, sharp);
+ return filament_rgb[a];
}
void main()
@@ -364,9 +311,6 @@ void main()
// world position and perturb the world normal.
vec3 triangle_normal = normalize(cross(dFdx(world_pos.xyz), dFdy(world_pos.xyz)));
vec3 tex_pos = world_pos.xyz - tex_anchor; // the frame the texture is projected in, as the bake does
- // World mm per pixel, for pre-filtering the interleave in printed_color(). Taken here because the
- // albedo branch at the end of main() is non-uniform control flow, where derivatives are undefined.
- vec3 pos_fwidth = fwidth(world_pos.xyz);
if (volume_mirrored)
triangle_normal = -triangle_normal;
@@ -508,6 +452,6 @@ void main()
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
- albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb), tex_pos, triangle_normal, pos_fwidth);
+ albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
diff --git a/src/libslic3r/CMakeLists.txt b/src/libslic3r/CMakeLists.txt
index c909d7e96a..ca95cfb364 100644
--- a/src/libslic3r/CMakeLists.txt
+++ b/src/libslic3r/CMakeLists.txt
@@ -180,6 +180,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp
+ Fill/FillTpmsAdaptive.cpp
+ Fill/FillTpmsAdaptive.hpp
Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp
diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp
index 2f23917be6..146d3ee7b4 100644
--- a/src/libslic3r/Fill/Fill.cpp
+++ b/src/libslic3r/Fill/Fill.cpp
@@ -311,6 +311,11 @@ struct SurfaceFillParams
// For Gyroid: when true, use the parameterized "optimized" wave.
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;
+
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -353,6 +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(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -386,6 +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->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
@@ -994,15 +1005,23 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p
// Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
- // 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).
- // Stored on SurfaceFillParams; copied to FillParams during conversion.
- params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized;
- // Orca: Likewise separated_infills only where it can move the pattern.
+ // Orca: Pass through separated_infills only where it can move the pattern.
params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) &&
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;
+
+ // 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;
+
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
region_config.sparse_infill_rotate_template.value);
@@ -1354,6 +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->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1403,6 +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.smooth_factor = surface_fill.params.smooth_factor;
// BBS
@@ -1575,6 +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->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
@@ -1614,6 +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.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
diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp
index 45dcfc87e8..e0308c9450 100644
--- a/src/libslic3r/Fill/FillBase.hpp
+++ b/src/libslic3r/Fill/FillBase.hpp
@@ -33,6 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r {
class Surface;
+class TpmsRadialField;
enum InfillPattern : int;
namespace FillAdaptive {
@@ -91,6 +92,11 @@ struct FillParams
// For Gyroid: when true, use the parameterized "optimized" variant.
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 };
+
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -155,6 +161,9 @@ public:
// Octree builds on mesh for usage in the adaptive cubic infill
FillAdaptive::Octree* adapt_fill_octree = nullptr;
+ // Radial coordinate inside the object for the adaptive TPMS infill
+ const TpmsRadialField* tpms_radial_field = nullptr;
+
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function.
const PrintConfig *print_config = nullptr;
diff --git a/src/libslic3r/Fill/FillGyroid.cpp b/src/libslic3r/Fill/FillGyroid.cpp
index 3b2803738c..06cb55f70e 100644
--- a/src/libslic3r/Fill/FillGyroid.cpp
+++ b/src/libslic3r/Fill/FillGyroid.cpp
@@ -17,6 +17,17 @@
#include "libslic3r/Polyline.hpp"
#include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp"
+#include "libslic3r/PrintConfig.hpp"
+#include "FillTpmsAdaptive.hpp"
+
+namespace Slic3r {
+
+static float gyroid(float x, float y, float z)
+{
+ return std::sin(x) * std::cos(y) + std::sin(y) * std::cos(z) + std::sin(z) * std::cos(x);
+}
+
+} // namespace Slic3r
// ---------------------------------------------------------------------------
// Marching-squares scalar field for the optimized gyroid branch.
@@ -62,10 +73,7 @@ struct GyroidField
float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const
{
- const float a = fx * float(x);
- const float b = fy * float(y);
- const float c = fz * float(z_arg);
- return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a);
+ return gyroid(fx * float(x), fy * float(y), fz * float(z_arg));
}
float get_scalar(Coord p) const
@@ -307,6 +315,14 @@ 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,
+ [&](const FillParams &shell_params, const ExPolygon &shell) {
+ this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
+ });
+ return;
+ }
+
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
if(std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle);
@@ -326,7 +342,12 @@ void FillGyroid::_fill_surface_single(
// generate pattern
Polylines polylines;
- if (params.gyroid_optimized) {
+ if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
+ // Radians per mm of the regular pattern at a density.
+ auto frequency = [¶ms, 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);
+ } 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.
// omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
diff --git a/src/libslic3r/Fill/FillTpmsAdaptive.cpp b/src/libslic3r/Fill/FillTpmsAdaptive.cpp
new file mode 100644
index 0000000000..2dcd2930b9
--- /dev/null
+++ b/src/libslic3r/Fill/FillTpmsAdaptive.cpp
@@ -0,0 +1,823 @@
+#include "FillTpmsAdaptive.hpp"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+
+#include "../BoundingBox.hpp"
+#include "../ClipperUtils.hpp"
+#include "../ExPolygon.hpp"
+#include "FillBase.hpp"
+#include "../Execution/ExecutionTBB.hpp"
+#include "../MarchingSquares.hpp"
+#include "../Point.hpp"
+#include "../Polygon.hpp"
+#include "../Polyline.hpp"
+#include "../PrintConfig.hpp"
+#include "../libslic3r.h"
+
+namespace Slic3r {
+
+namespace {
+
+// At most 4 MB of body indices; finer cells would not change the grading.
+constexpr double MaxNodes = double(1 << 20);
+constexpr double MinCellSize = 0.5;
+
+// Two deepest points are in separate lobes when the depth between them drops below this ratio of the shallower one.
+constexpr double NeckRatio = 0.8;
+// Lobes shallower than this ratio of the deepest one of their body are graded as part of it.
+constexpr double MinLobeRatio = 0.3;
+// A lobe reaches twice as far as the side towards its neighbour, so that the side is half way to the surface.
+constexpr double LobeReach = 2.;
+// Width of the morph between the patterns of two lobes, in their distance to the center over its depth.
+constexpr double LobeMorph = 0.1;
+// Densities of the levels of Stepped shells, and of Smooth blend, are at most these ratios apart. Fewer levels blend
+// with fewer lines running along the blends.
+constexpr double ShellRatio = 1.5;
+constexpr double BlendRatio = 2.5;
+// Distance warp: samples of the mean depth along a ray.
+constexpr int Samples = 32;
+
+constexpr float InfF = std::numeric_limits::infinity();
+constexpr double InfD = std::numeric_limits::infinity();
+
+// Squared distance transform of a line (Felzenszwalb & Huttenlocher); infinite samples are no sites.
+void distance_transform_line(const float *f, float *d, int n, int *v, double *s)
+{
+ int k = -1;
+ for (int q = 0; q < n; ++q) {
+ if (f[q] == InfF)
+ continue;
+ double x = -InfD;
+ while (k >= 0) {
+ x = (f[q] + double(q) * q - f[v[k]] - double(v[k]) * v[k]) / (2. * (q - v[k]));
+ if (x > s[k])
+ break;
+ --k;
+ }
+ if (k < 0)
+ x = -InfD;
+ v[++k] = q;
+ s[k] = x;
+ s[k + 1] = InfD;
+ }
+ if (k < 0) {
+ std::fill(d, d + n, InfF);
+ return;
+ }
+ for (int q = 0, j = 0; q < n; ++q) {
+ while (s[j + 1] < q)
+ ++j;
+ d[q] = float(sqr(double(q - v[j])) + f[v[j]]);
+ }
+}
+
+void distance_transform_axis(std::vector &grid, const Vec3i32 &size, int axis, const std::function &throw_if_canceled)
+{
+ const int n = size[axis];
+ const int a1 = (axis + 1) % 3;
+ const int a2 = (axis + 2) % 3;
+ const size_t stride = axis == 0 ? 1 : axis == 1 ? size_t(size.x()) : size_t(size.x()) * size.y();
+ tbb::parallel_for(tbb::blocked_range(0, size_t(size[a1]) * size[a2]), [&](const tbb::blocked_range &range) {
+ std::vector f(n), d(n);
+ std::vector v(n);
+ std::vector s(n + 1);
+ for (size_t line = range.begin(); line < range.end(); ++line) {
+ Vec3i32 idx;
+ idx[axis] = 0;
+ idx[a1] = int(line % size[a1]);
+ idx[a2] = int(line / size[a1]);
+ const size_t first = (size_t(idx.z()) * size.y() + idx.y()) * size.x() + idx.x();
+ for (int i = 0; i < n; ++i)
+ f[i] = grid[first + i * stride];
+ distance_transform_line(f.data(), d.data(), n, v.data(), s.data());
+ for (int i = 0; i < n; ++i)
+ grid[first + i * stride] = d[i];
+ }
+ throw_if_canceled();
+ });
+}
+
+// Marks the nodes inside the expolygons with infinity, by even-odd scanlines.
+void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, int nx, int ny, float *nodes)
+{
+ std::vector> crossings(ny);
+ auto add_crossings = [&](const Polygon &polygon) {
+ const Points &pts = polygon.points;
+ for (size_t i = 0; i < pts.size(); ++i) {
+ const Vec2d a = unscaled(pts[i]);
+ const Vec2d b = unscaled(pts[i + 1 == pts.size() ? 0 : i + 1]);
+ if (a.y() == b.y())
+ continue;
+ const auto [lo, hi] = std::minmax(a.y(), b.y());
+ const int j0 = std::max(0, int(std::ceil((lo - origin.y()) / cell)));
+ const int j1 = std::min(ny, int(std::ceil((hi - origin.y()) / cell)));
+ for (int j = j0; j < j1; ++j) {
+ const double y = origin.y() + j * cell;
+ crossings[j].push_back(a.x() + (b.x() - a.x()) * (y - a.y()) / (b.y() - a.y()));
+ }
+ }
+ };
+ for (const ExPolygon &expolygon : expolygons) {
+ add_crossings(expolygon.contour);
+ for (const Polygon &hole : expolygon.holes)
+ add_crossings(hole);
+ }
+ for (int j = 0; j < ny; ++j) {
+ std::vector &xs = crossings[j];
+ std::sort(xs.begin(), xs.end());
+ for (size_t k = 0; k + 1 < xs.size(); k += 2) {
+ const int i0 = std::max(0, int(std::ceil((xs[k] - origin.x()) / cell)));
+ const int i1 = std::min(nx, int(std::ceil((xs[k + 1] - origin.x()) / cell)));
+ std::fill(nodes + size_t(j) * nx + std::min(i0, i1), nodes + size_t(j) * nx + i1, InfF);
+ }
+ }
+}
+
+// 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)
+{
+ switch (gradient) {
+ case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
+ case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
+ default: return 1. + (ratio - 1.) * depth;
+ }
+}
+
+// Levels of Stepped shells and Smooth blend, geometric from the surface at 0 to the interior at count, and the
+// continuous level of the target of a depth.
+struct DensityLevels
+{
+ DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
+ : ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
+ {}
+
+ double scale(int level) const { return count == 0 ? 1. : std::pow(ratio, double(level) / count); }
+ double level(double depth) const
+ {
+ return count == 0 ? 0. : count * std::log(target_scale(ratio, gradient, depth)) / std::log(ratio);
+ }
+
+ double ratio;
+ TpmsAdaptiveGradient gradient;
+ int count;
+};
+
+// Scale of the pattern around the center at a radial coordinate t. The mean cell scale over the ball of radius t,
+// t^-3 * integral of 3 t'^2 * target(t'), or over the disc in 2D, follows the gradient; beyond the surface the target
+// is the surface scale.
+class RadialScale
+{
+public:
+ RadialScale(const AdaptiveTpms &tpms, int dimensions) : m_dimensions(dimensions)
+ {
+ const double ratio = std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3);
+ auto target = [&tpms, ratio](double depth) { return target_scale(ratio, tpms.gradient, depth); };
+ m_scale[0] = target(1.);
+ double volume = 0.;
+ for (size_t i = 1; i < m_scale.size(); ++i) {
+ const double t0 = double(i - 1) / double(m_scale.size() - 1);
+ const double t1 = double(i) / double(m_scale.size() - 1);
+ volume += (std::pow(t1, m_dimensions) - std::pow(t0, m_dimensions)) * target(1. - 0.5 * (t0 + t1));
+ m_scale[i] = volume / std::pow(t1, m_dimensions);
+ }
+ }
+
+ double operator()(double t) const
+ {
+ if (t >= 1.) {
+ const double volume = std::pow(t, m_dimensions);
+ return (m_scale.back() + volume - 1.) / volume;
+ }
+ const double x = t * double(m_scale.size() - 1);
+ const size_t i = std::min(size_t(x), m_scale.size() - 2);
+ return m_scale[i] + (m_scale[i + 1] - m_scale[i]) * (x - double(i));
+ }
+
+private:
+ int m_dimensions;
+ std::array m_scale;
+};
+
+} // namespace
+
+TpmsRadialField::TpmsRadialField(const std::vector &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
+ const std::function &throw_if_canceled)
+ : m_mode(mode)
+ , m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
+{
+ assert(!slices.empty() && mode != TpmsAdaptiveMode::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;
+
+ // Padded by a node on each side, so that the border of the grid is outside.
+ m_cell = std::max(MinCellSize, std::cbrt(extent.prod() / MaxNodes));
+ auto nodes = [this](double length) { return int(std::ceil(length / m_cell)) + 3; };
+ while (double(nodes(extent.x())) * nodes(extent.y()) * nodes(extent.z()) > MaxNodes)
+ m_cell *= 1.1;
+ m_size = Vec3i32(nodes(extent.x()), nodes(extent.y()), nodes(extent.z()));
+ m_origin = min - Vec3d::Constant(m_cell);
+
+ const size_t sy = size_t(m_size.x());
+ const size_t sz = sy * m_size.y();
+ std::vector depth(sz * m_size.z(), 0.f);
+ tbb::parallel_for(tbb::blocked_range(0, m_size.z()), [&](const tbb::blocked_range &range) {
+ for (int k = range.begin(); k < range.end(); ++k) {
+ const double z = m_origin.z() + k * m_cell;
+ auto it = std::lower_bound(slices.begin(), slices.end(), z, [](const Slice &s, double z) { return s.top_z < z; });
+ if (it != slices.end() && z > it->bottom_z)
+ rasterize(*it->expolygons, m_origin.head<2>(), m_cell, m_size.x(), m_size.y(), depth.data() + k * sz);
+ }
+ throw_if_canceled();
+ });
+ for (int axis = 0; axis < 3; ++axis)
+ if (axis != m_axis)
+ distance_transform_axis(depth, m_size, axis, throw_if_canceled);
+
+ auto position = [this, sy, sz](size_t i) {
+ return Vec3d(m_origin + m_cell * Vec3d(double(i % sy), double(i / sy % m_size.y()), double(i / sz)));
+ };
+ const std::array steps{1, -1, std::ptrdiff_t(sy), -std::ptrdiff_t(sy), std::ptrdiff_t(sz), -std::ptrdiff_t(sz)};
+
+ auto node_of = [this, sy, sz](const Vec3d &pt) -> std::ptrdiff_t {
+ const Vec3d f = (pt - m_origin) / m_cell;
+ const long x = std::lround(f.x()), y = std::lround(f.y()), z = std::lround(f.z());
+ if (x < 0 || y < 0 || z < 0 || x >= m_size.x() || y >= m_size.y() || z >= m_size.z())
+ return -1;
+ return std::ptrdiff_t(size_t(z) * sz + size_t(y) * sy + size_t(x));
+ };
+ // In the 2D modes, the steps within a section.
+ auto in_section = [this](size_t step) { return int(step / 2) != m_axis; };
+ std::vector neighbours;
+ for (int dz = -1; dz <= 1; ++dz)
+ for (int dy = -1; dy <= 1; ++dy)
+ for (int dx = -1; dx <= 1; ++dx)
+ if ((dx != 0 || dy != 0 || dz != 0) && (m_axis < 0 || Vec3i32(dx, dy, dz)[m_axis] == 0))
+ neighbours.push_back(std::ptrdiff_t(dz) * std::ptrdiff_t(sz) + std::ptrdiff_t(dy) * std::ptrdiff_t(sy) + dx);
+
+ // 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;
+ m_body.assign(depth.size(), -1);
+ std::vector body_nodes;
+ for (size_t seed = 0; seed < depth.size(); ++seed) {
+ if (depth[seed] == 0.f || m_body[seed] >= 0)
+ continue;
+ const int id = int(m_bodies.size());
+ body_nodes.assign(1, seed);
+ m_body[seed] = id;
+ float max_depth = 0.f;
+ for (size_t k = 0; k < body_nodes.size(); ++k) {
+ max_depth = std::max(max_depth, depth[body_nodes[k]]);
+ for (size_t s = 0; s < steps.size(); ++s)
+ if (const size_t j = body_nodes[k] + steps[s]; in_section(s) && depth[j] > 0.f && m_body[j] < 0) {
+ m_body[j] = id;
+ body_nodes.push_back(j);
+ }
+ }
+ m_bodies.push_back({m_lobes.size(), 0, (std::sqrt(double(max_depth)) - 0.5) * m_cell});
+ if (!lobes)
+ continue;
+
+ std::vector peaks;
+ for (size_t i : body_nodes)
+ if (depth[i] >= sqr(MinLobeRatio) * max_depth &&
+ std::all_of(neighbours.begin(), neighbours.end(), [&](std::ptrdiff_t n) { return depth[i + n] <= depth[i]; }))
+ peaks.push_back(i);
+ std::sort(peaks.begin(), peaks.end(), [&depth](size_t a, size_t b) { return depth[a] > depth[b] || (depth[a] == depth[b] && a < b); });
+ auto necked = [&](size_t a, size_t b) {
+ const Vec3d pa = position(a), pb = position(b);
+ const double limit = sqr(NeckRatio) * std::min(depth[a], depth[b]);
+ const int samples = int(std::ceil((pb - pa).norm() / (0.5 * m_cell)));
+ for (int s = 1; s < samples; ++s)
+ if (depth[node_of(pa + (pb - pa) * (double(s) / samples))] < limit)
+ return true;
+ return false;
+ };
+ // A peak joins the first lobe it sees without a neck, if it is as deep. The lobes are made one at a time,
+ // from the first remaining peak, testing the others in parallel.
+ std::vector> ties;
+ while (!peaks.empty()) {
+ const size_t front = peaks.front();
+ std::vector joins(peaks.size(), 0);
+ tbb::parallel_for(tbb::blocked_range(1, peaks.size()), [&](const tbb::blocked_range &range) {
+ for (size_t k = range.begin(); k < range.end(); ++k)
+ if (!necked(front, peaks[k]))
+ joins[k] = std::sqrt(depth[peaks[k]]) >= std::sqrt(depth[front]) - 1.f ? 1 : 2;
+ });
+ std::vector &tied = ties.emplace_back(1, front);
+ std::vector remaining;
+ for (size_t k = 1; k < peaks.size(); ++k)
+ if (joins[k] == 0)
+ remaining.push_back(peaks[k]);
+ else if (joins[k] == 1)
+ tied.push_back(peaks[k]);
+ peaks = std::move(remaining);
+ }
+ m_bodies.back().lobes = ties.size();
+ for (const std::vector &tied : ties) {
+ Vec3d middle(0., 0., 0.);
+ for (size_t i : tied)
+ middle += position(i);
+ middle /= double(tied.size());
+ Vec3d center = position(tied.front());
+ for (size_t i : tied)
+ if ((position(i) - middle).squaredNorm() < (center - middle).squaredNorm())
+ center = position(i);
+ m_lobes.push_back({center, (std::sqrt(double(depth[tied.front()])) - 0.5) * m_cell, {}});
+ }
+ }
+ throw_if_canceled();
+
+ if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
+ m_depth.assign(depth.size(), 0.f);
+ for (size_t i = 0; i < depth.size(); ++i)
+ if (m_body[i] >= 0)
+ m_depth[i] = float(std::min(1., std::max(0., std::sqrt(double(depth[i])) - 0.5) * m_cell / m_bodies[m_body[i]].depth));
+ }
+
+ // The reach of a lobe is the first exit along each direction from its center, smoothed over the directions.
+ // It is shortened towards a neighbouring lobe, from where the point is nearer to the other lobe relative to their depths.
+ std::vector lobe_body(m_lobes.size());
+ for (size_t id = 0; id < m_bodies.size(); ++id)
+ std::fill_n(lobe_body.begin() + m_bodies[id].first_lobe, m_bodies[id].lobes, int(id));
+ const int rows = this->directions() / Azimuth;
+ auto direction = [this](int i, int j) {
+ const double azimuth = j * 2. * PI / Azimuth;
+ Vec3d dir = Vec3d::Zero();
+ if (m_axis < 0) {
+ const double polar = (i + 0.5) * PI / Polar;
+ dir = Vec3d(std::sin(polar) * std::cos(azimuth), std::sin(polar) * std::sin(azimuth), std::cos(polar));
+ } else {
+ dir[(m_axis + 1) % 3] = std::cos(azimuth);
+ dir[(m_axis + 2) % 3] = std::sin(azimuth);
+ }
+ return dir;
+ };
+ // Two passes of a box filter over the neighbouring directions, for each of the values of a direction.
+ auto smooth = [rows](std::vector &values, size_t count) {
+ for (int pass = 0; pass < 2; ++pass) {
+ std::vector smoothed(values.size(), 0.);
+ for (int i = 0; i < rows; ++i)
+ for (int j = 0; j < Azimuth; ++j)
+ for (size_t k = 0; k < count; ++k) {
+ double &sum = smoothed[size_t(i * Azimuth + j) * count + k];
+ for (int di = -1; di <= 1; ++di)
+ for (int dj = -1; dj <= 1; ++dj)
+ sum += values[size_t(std::clamp(i + di, 0, rows - 1) * Azimuth + (j + dj + Azimuth) % Azimuth) * count + k];
+ sum /= 9.;
+ }
+ values = std::move(smoothed);
+ }
+ };
+ tbb::parallel_for(tbb::blocked_range(0, m_lobes.size()), [&](const tbb::blocked_range &range) {
+ for (size_t l = range.begin(); l < range.end(); ++l) {
+ const int id = lobe_body[l];
+ const Body &body = m_bodies[id];
+ Lobe &lobe = m_lobes[l];
+ // The other lobes of the body, by the distance from the center beyond which they may be nearer.
+ std::vector> others;
+ for (size_t k = body.first_lobe; k < body.first_lobe + body.lobes; ++k)
+ if (k != l)
+ others.emplace_back((m_lobes[k].center - lobe.center).norm() / (1. + m_lobes[k].depth / lobe.depth), k);
+ std::sort(others.begin(), others.end());
+ auto nearer_lobe = [&](const Vec3d &pt, double r) {
+ for (auto it = others.begin(); it != others.end() && it->first < r; ++it)
+ if ((pt - m_lobes[it->second].center).norm() / m_lobes[it->second].depth < r / lobe.depth)
+ return true;
+ return false;
+ };
+ const double step = 0.5 * m_cell;
+ std::vector log_reach(this->directions());
+ for (int i = 0; i < rows; ++i)
+ for (int j = 0; j < Azimuth; ++j) {
+ const Vec3d dir = direction(i, j);
+ double r = 0.;
+ double limit = InfD;
+ for (;;) {
+ const Vec3d pt = lobe.center + (r + step) * dir;
+ const std::ptrdiff_t n = node_of(pt);
+ if (n < 0 || depth[n] == 0.f || m_body[n] != id || r + step >= limit)
+ break;
+ if (limit == InfD && nearer_lobe(pt, r + step))
+ limit = LobeReach * (r + step);
+ r += step;
+ }
+ log_reach[i * Azimuth + j] = std::log(std::min(r + 0.5 * step, limit));
+ }
+ smooth(log_reach, 1);
+ 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) {
+ std::vector 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);
+ double integral = 0.;
+ double previous = this->depth(lobe.center);
+ mean[d * (Samples + 1)] = previous;
+ for (int k = 1; k <= Samples; ++k) {
+ // Exact for a depth linear between the samples, a + b * tau, weighted by tau^2.
+ const double t0 = double(k - 1) / Samples;
+ const double t1 = double(k) / Samples;
+ const double d1 = this->depth(lobe.center + t1 * lobe.reach[d] * dir);
+ const double b = (d1 - previous) * Samples;
+ const double a = previous - b * t0;
+ integral += a * (std::pow(t1, 3) - std::pow(t0, 3)) / 3. + b * (std::pow(t1, 4) - std::pow(t0, 4)) / 4.;
+ previous = d1;
+ mean[d * (Samples + 1) + k] = 3. * integral / std::pow(t1, 3);
+ }
+ }
+ // Smoothed like the reach: sharper profiles shear the pattern across the layer, adding lines.
+ smooth(mean, Samples + 1);
+ lobe.mean_depth.assign(mean.begin(), mean.end());
+ }
+ }
+ throw_if_canceled();
+ });
+
+ // Every other node belongs to its nearest body, within its section in the 2D modes.
+ if (m_axis >= 0) {
+ std::vector has_body(m_size[m_axis], false);
+ for (size_t i = 0; i < m_body.size(); ++i)
+ if (m_body[i] >= 0)
+ has_body[m_axis == 0 ? i % sy : m_axis == 1 ? i / sy % m_size.y() : i / sz] = true;
+ m_section.assign(m_size[m_axis], -1);
+ for (int k = 0; k < m_size[m_axis]; ++k)
+ for (int d = 0; d < m_size[m_axis] && m_section[k] < 0; ++d)
+ if (k - d >= 0 && has_body[k - d])
+ m_section[k] = k - d;
+ else if (k + d < m_size[m_axis] && has_body[k + d])
+ m_section[k] = k + d;
+ }
+ // Stepped shells and Smooth blend only look up the depth.
+ if (!lobes) {
+ m_body = {};
+ return;
+ }
+ if (m_bodies.size() == 1) {
+ std::fill(m_body.begin(), m_body.end(), 0);
+ return;
+ }
+ std::deque queue;
+ for (size_t i = 0; i < m_body.size(); ++i)
+ if (m_body[i] >= 0)
+ queue.push_back(i);
+ while (!queue.empty()) {
+ const size_t i = queue.front();
+ queue.pop_front();
+ const size_t x = i % sy, y = i / sy % m_size.y(), z = i / sz;
+ const std::array valid{x + 1 < sy, x > 0, y + 1 < size_t(m_size.y()), y > 0, z + 1 < size_t(m_size.z()), z > 0};
+ for (size_t k = 0; k < steps.size(); ++k)
+ if (valid[k] && in_section(k) && m_body[i + steps[k]] < 0) {
+ m_body[i + steps[k]] = m_body[i];
+ queue.push_back(i + steps[k]);
+ }
+ }
+}
+
+double TpmsRadialField::depth(const Vec3d &pt) const
+{
+ assert(!m_depth.empty());
+ const Vec3d f = (pt - m_origin) / m_cell;
+ std::array n0;
+ std::array w;
+ for (int a = 0; a < 3; ++a) {
+ n0[a] = std::clamp(int(std::floor(f[a])), 0, m_size[a] - 2);
+ w[a] = std::clamp(f[a] - n0[a], 0., 1.);
+ }
+ const size_t sy = size_t(m_size.x());
+ const size_t sz = sy * size_t(m_size.y());
+ double value = 0.;
+ for (int c = 0; c < 8; ++c) {
+ const size_t n = size_t(n0[2] + (c >> 2)) * sz + size_t(n0[1] + (c >> 1 & 1)) * sy + size_t(n0[0] + (c & 1));
+ value += (c & 1 ? w[0] : 1. - w[0]) * (c >> 1 & 1 ? w[1] : 1. - w[1]) * (c >> 2 ? w[2] : 1. - w[2]) * m_depth[n];
+ }
+ return value;
+}
+
+Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d ¢er) const
+{
+ Vec3d d = pt - center;
+ if (m_axis >= 0)
+ d[m_axis] = 0.;
+ return d;
+}
+
+double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
+{
+ const Vec3d d = this->offset(pt, lobe.center);
+ const double r = d.norm();
+ // Distance warp: the radial coordinate of the linear profile with the same mean depth, which is 1 - 3/4 of it.
+ auto warp = [](double mean_depth) { return std::max(0., 4. / 3. * (1. - mean_depth)); };
+ if (r < EPSILON)
+ return lobe.mean_depth.empty() ? 0. : warp(lobe.mean_depth.front());
+ int i = 0;
+ double fi = 0.;
+ double azimuth;
+ if (m_axis < 0) {
+ const double polar = std::clamp(std::acos(std::clamp(d.z() / r, -1., 1.)) / PI * Polar - 0.5, 0., double(Polar - 1));
+ i = std::min(int(polar), Polar - 2);
+ fi = polar - i;
+ azimuth = std::atan2(d.y(), d.x());
+ } else
+ azimuth = std::atan2(d[(m_axis + 2) % 3], d[(m_axis + 1) % 3]);
+ azimuth *= Azimuth / (2. * PI);
+ if (azimuth < 0.)
+ azimuth += Azimuth;
+ const int j0 = int(azimuth) % Azimuth;
+ const int j1 = (j0 + 1) % Azimuth;
+ const double fj = azimuth - std::floor(azimuth);
+ auto at = [&lobe](int i, int j) { return double(lobe.reach[i * Azimuth + j]); };
+ double reach = at(i, j0) * (1. - fj) + at(i, j1) * fj;
+ if (fi > 0.)
+ reach = reach * (1. - fi) + (at(i + 1, j0) * (1. - fj) + at(i + 1, j1) * fj) * fi;
+ const double tau = r / reach;
+ if (lobe.mean_depth.empty())
+ return tau;
+ // Beyond the surface, the depth is zero.
+ auto mean_at = [&lobe, tau](int i, int j) {
+ const float *mean = lobe.mean_depth.data() + size_t(i * Azimuth + j) * (Samples + 1);
+ if (tau >= 1.)
+ return double(mean[Samples]) / (tau * tau * tau);
+ const double x = tau * Samples;
+ const int k = std::min(int(x), Samples - 1);
+ return mean[k] + (mean[k + 1] - mean[k]) * (x - k);
+ };
+ double mean = mean_at(i, j0) * (1. - fj) + mean_at(i, j1) * fj;
+ if (fi > 0.)
+ mean = mean * (1. - fi) + (mean_at(i + 1, j0) * (1. - fj) + mean_at(i + 1, j1) * fj) * fi;
+ return warp(mean);
+}
+
+size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
+{
+ assert(!this->empty());
+ Vec3i32 idx;
+ for (int axis = 0; axis < 3; ++axis)
+ idx[axis] = std::clamp(int(std::lround((pt[axis] - m_origin[axis]) / m_cell)), 0, m_size[axis] - 1);
+ auto node = [this](const Vec3i32 &idx) { return (size_t(idx.z()) * m_size.y() + idx.y()) * m_size.x() + idx.x(); };
+ if (m_axis < 0)
+ return this->body_radial(node(idx), pt, 1.f, out.data());
+
+ // The sections around pt, or the nearest ones with a body.
+ const double f = std::clamp((pt[m_axis] - m_origin[m_axis]) / m_cell, 0., double(m_size[m_axis] - 1));
+ const int k = std::min(int(f), m_size[m_axis] - 2);
+ const float w = float(f - k);
+ size_t count = 0;
+ if (w < 1.f) {
+ idx[m_axis] = m_section[k];
+ count += this->body_radial(node(idx), pt, 1.f - w, out.data());
+ }
+ if (w > 0.f) {
+ idx[m_axis] = m_section[k + 1];
+ count += this->body_radial(node(idx), pt, w, out.data() + count);
+ }
+ return count;
+}
+
+size_t TpmsRadialField::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) {
+ const Lobe &lobe = m_lobes[body.first_lobe];
+ out[0] = {lobe.center, radial(lobe, pt), weight};
+ return 1;
+ }
+
+ // The lobes nearest relative to their depth; they morph into each other near the sides where they are as near.
+ std::array, MaxMorph> nearest;
+ size_t count = 0;
+ for (size_t l = body.first_lobe; l < body.first_lobe + body.lobes; ++l) {
+ const double d = this->offset(pt, m_lobes[l].center).norm() / m_lobes[l].depth;
+ if (count < MaxMorph)
+ nearest[count++] = {d, l};
+ else if (d < nearest.back().first)
+ nearest.back() = {d, l};
+ else
+ continue;
+ for (size_t k = count - 1; k > 0 && nearest[k].first < nearest[k - 1].first; --k)
+ std::swap(nearest[k], nearest[k - 1]);
+ }
+ std::array blend;
+ double total = 0.;
+ size_t morphs = 0;
+ for (; morphs < count; ++morphs) {
+ const double u = 0.5 - (nearest[morphs].first - nearest[0].first) / LobeMorph;
+ if (u <= 0.)
+ break;
+ blend[morphs] = u * u * (3. - 2. * u);
+ total += blend[morphs];
+ }
+ for (size_t k = 0; k < morphs; ++k) {
+ const Lobe &lobe = m_lobes[nearest[k].second];
+ out[k] = {lobe.center, radial(lobe, pt), float(weight * blend[k] / total)};
+ }
+ return morphs;
+}
+
+} // namespace Slic3r
+
+namespace marchsq {
+using namespace Slic3r;
+
+struct AdaptiveTpmsField
+{
+ static constexpr float gsizef = 0.40f; // grid cell size in mm (roughly line segment length).
+ static constexpr float rsizef = 0.004f; // raster pixel size in mm (roughly point accuracy).
+ const coord_t rsize = scaled(rsizef);
+ const long gsize = std::lround(gsizef / rsizef);
+
+ const AdaptiveTpms &tpms;
+ const TpmsRadialField &radial_field;
+ RadialScale scale;
+ DensityLevels levels;
+ Point size;
+ Point offs;
+ double z;
+ double cos_angle;
+ double sin_angle;
+
+ AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &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)
+ , cos_angle(std::cos(angle)), sin_angle(std::sin(angle))
+ {}
+
+ // The pattern is scaled around the center of the lobe, morphing into the pattern of a neighbouring lobe near the
+ // side between them. In the 2D modes only within the section, with the interior frequency along the axis.
+ // The radial field is in the object frame, the fill is rotated by -angle.
+ float get_scalar(const Coord &p) const
+ {
+ const Point pt = to_Point(p);
+ 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) {
+ // 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);
+ return tpms.equation(float(frequency * x), float(frequency * y), float(frequency * z));
+ };
+ if (levels.count == 0)
+ return lattice(0);
+ const double c = std::clamp(levels.level(radial_field.depth(obj)), 0., double(levels.count));
+ const int k = std::min(int(c), levels.count - 1);
+ const double u = c - k;
+ const double w = u * u * (3. - 2. * u);
+ 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;
+ const size_t count = radial_field.radial(obj, radials);
+ float value = 0.f;
+ for (size_t i = 0; i < count; ++i) {
+ const auto &[center, t, weight] = radials[i];
+ Vec3d q = tpms.surface_frequency * scale(t) * (obj - center);
+ if (axis >= 0)
+ q[axis] = tpms.interior_frequency * obj[axis];
+ value += weight * tpms.equation(float(cos_angle * q.x() + sin_angle * q.y()), float(cos_angle * q.y() - sin_angle * q.x()), float(q.z()));
+ }
+ return value;
+ }
+
+ inline coord_t to_coord(long x) const { return x * rsize; }
+ inline long to_coordr(coord_t x) const { return x / rsize; }
+ 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
+{
+ using ValueType = float;
+ static float get(const AdaptiveTpmsField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
+ static size_t rows(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.y()); }
+ static size_t cols(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.x()); }
+};
+
+// Continuous density level of the depth over a layer, in the object frame.
+struct TpmsLevelField
+{
+ 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 DensityLevels &levels;
+ Point size;
+ Point offs;
+ double z;
+
+ TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
+ : field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
+ {}
+
+ float get_scalar(const Coord &p) const
+ {
+ const Point pt = to_Point(p);
+ return float(levels.level(field.depth(Vec3d(unscaled(pt.x()), unscaled(pt.y()), z))));
+ }
+
+ inline coord_t to_coord(long x) const { return x * rsize; }
+ inline long to_coordr(coord_t x) const { return x / rsize; }
+ 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
+{
+ 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()); }
+};
+
+} // namespace marchsq
+
+namespace Slic3r {
+
+Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &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.
+ const coord_t cell = scaled(marchsq::AdaptiveTpmsField::gsizef);
+ bbox.offset(cell);
+ bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
+ const marchsq::AdaptiveTpmsField raster(tpms, field, bbox, z - 0.5 * layer_height, angle);
+ const std::vector rings = marchsq::execute_with_policy(ex_tbb, raster, 0.f, {raster.gsize, raster.gsize});
+
+ // Loops narrower than two lines print as blobs.
+ const double min_loop_length = scaled(2. * PI * spacing);
+ Polylines polylines;
+ polylines.reserve(rings.size());
+ for (const marchsq::Ring &ring : rings) {
+ Polyline polyline;
+ polyline.points.reserve(ring.size() + 1);
+ for (const marchsq::Coord &crd : ring)
+ polyline.points.emplace_back(raster.to_Point(crd));
+ polyline.points.push_back(polyline.points.front());
+ polyline.simplify(SCALED_SPARSE_INFILL_RESOLUTION);
+ if (polyline.length() >= min_loop_length)
+ polylines.push_back(std::move(polyline));
+ }
+ return polylines;
+}
+
+std::vector make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
+ float surface_density, float interior_density, TpmsAdaptiveGradient 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);
+ 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);
+
+ // Each level takes the part deeper than the middle between it and the previous one.
+ std::vector shells;
+ ExPolygons remaining{expolygon};
+ for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
+ Polygons deeper;
+ for (const marchsq::Ring &ring : marchsq::execute_with_policy(ex_tbb, raster, float(level + 0.5), {raster.gsize, raster.gsize})) {
+ Polygon &polygon = deeper.emplace_back();
+ polygon.points.reserve(ring.size());
+ for (const marchsq::Coord &crd : ring)
+ polygon.points.emplace_back(raster.to_Point(crd));
+ }
+ ExPolygons inner = intersection_ex(union_ex(deeper), remaining);
+ shells.push_back({float(surface_density * levels.scale(level)), diff_ex(remaining, inner)});
+ remaining = std::move(inner);
+ }
+ if (!remaining.empty())
+ shells.push_back({interior_density, std::move(remaining)});
+ return shells;
+}
+
+void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams ¶ms, coordf_t spacing,
+ const std::function &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.density = shell.density;
+ for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
+ fill_shell(shell_params, part);
+ }
+}
+
+} // namespace Slic3r
diff --git a/src/libslic3r/Fill/FillTpmsAdaptive.hpp b/src/libslic3r/Fill/FillTpmsAdaptive.hpp
new file mode 100644
index 0000000000..8e7b9db6cd
--- /dev/null
+++ b/src/libslic3r/Fill/FillTpmsAdaptive.hpp
@@ -0,0 +1,143 @@
+#pragma once
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "../libslic3r.h"
+#include "FillBase.hpp"
+#include "../BoundingBox.hpp"
+#include "../ExPolygon.hpp"
+#include "../Point.hpp"
+#include "../Polyline.hpp"
+#include "../PrintConfig.hpp"
+
+namespace Slic3r {
+
+// Radial coordinate inside the lobes of the bodies of an object, sampled from its slices: 0 at the center of a
+// 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
+{
+public:
+ struct Slice
+ {
+ coordf_t bottom_z;
+ coordf_t top_z;
+ const ExPolygons *expolygons;
+ };
+
+ struct Radial
+ {
+ Vec3d center;
+ double t;
+ float weight;
+ };
+
+ // Slices sorted by z, in the XY coordinates of the fill and the print Z.
+ TpmsRadialField(const std::vector &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
+ const std::function &throw_if_canceled);
+
+ // Lobes blended near the sides between them, from a body or from each of the two sections around a point.
+ static constexpr size_t MaxMorph = 4;
+ using Radials = std::array;
+
+ // Without a body, as when the object is thinner than the grid cells.
+ bool empty() const { return m_bodies.empty(); }
+
+ // Radial coordinates of pt in unscaled coordinates towards the lobe it belongs to, and towards the neighbouring
+ // lobes near the sides between them, with weights summing to 1. In the 2D modes, those of the two sections around
+ // pt. Returns their count.
+ size_t radial(const Vec3d &pt, Radials &out) const;
+
+ // 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; }
+
+ // 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.
+ double depth(const Vec3d &pt) const;
+
+private:
+ struct Lobe
+ {
+ Vec3d center;
+ double depth;
+ // Distance from the center to the surface on a latitude-longitude grid of directions.
+ std::vector reach;
+ // Distance warp: mean depth over the ball along each direction, sampled up to the reach.
+ std::vector mean_depth;
+ };
+
+ struct Body
+ {
+ size_t first_lobe;
+ size_t lobes;
+ // Distance from the deepest point to the surface.
+ double depth;
+ };
+
+ double radial(const Lobe &lobe, const Vec3d &pt) const;
+ size_t body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const;
+ // Offset of pt from a center, within the section in the 2D modes.
+ Vec3d offset(const Vec3d &pt, const Vec3d ¢er) const;
+ int directions() const { return m_axis < 0 ? Polar * Azimuth : Azimuth; }
+
+ // Directions of the reach of a lobe, on a latitude-longitude grid, or a circle in the 2D modes.
+ static constexpr int Polar = 24;
+ static constexpr int Azimuth = 48;
+
+ TpmsAdaptiveMode m_mode;
+ int m_axis;
+ Vec3d m_origin;
+ double m_cell;
+ Vec3i32 m_size;
+ // Nearest body of every grid node.
+ std::vector m_body;
+ std::vector m_bodies;
+ std::vector m_lobes;
+ // In the 2D modes, the nearest section with a body to every section.
+ std::vector m_section;
+ // In the modes following the distance to the surface, the depth of every grid node.
+ std::vector m_depth;
+};
+
+using TpmsRadialFieldPtr = std::unique_ptr;
+// A field for every adaptive mode in use, indexed by the mode.
+using TpmsRadialFields = std::array;
+
+struct AdaptiveTpms
+{
+ // Implicit TPMS equation with a period of 2 PI.
+ float (*equation)(float x, float y, float z);
+ // Pattern frequencies at the surface and at the center, in radians per mm.
+ double surface_frequency;
+ double interior_frequency;
+ TpmsAdaptiveGradient 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,
+ coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
+
+struct TpmsShell
+{
+ float density;
+ ExPolygons expolygons;
+};
+
+// 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 make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
+ float surface_density, float interior_density, TpmsAdaptiveGradient 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 ¶ms, coordf_t spacing,
+ const std::function &fill_shell);
+
+} // namespace Slic3r
diff --git a/src/libslic3r/Fill/FillTpmsD.cpp b/src/libslic3r/Fill/FillTpmsD.cpp
index bb1a338691..04f8222ca5 100644
--- a/src/libslic3r/Fill/FillTpmsD.cpp
+++ b/src/libslic3r/Fill/FillTpmsD.cpp
@@ -14,8 +14,10 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
+#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp"
+#include "FillTpmsAdaptive.hpp"
namespace Slic3r {
@@ -23,6 +25,11 @@ static double scaled_floor(double x,double scale){
return std::floor(x/scale)*scale;
}
+static float schwarz_d(float x, float y, float z)
+{
+ return std::sin(x) * std::sin(y) * std::sin(z) - std::cos(x) * std::cos(y) * std::cos(z);
+}
+
static Polylines make_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
{
const double scaleFactor = scale_(line_spacing) / density_adjusted;
@@ -110,31 +117,49 @@ 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,
+ [&](const FillParams &shell_params, const ExPolygon &shell) {
+ this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
+ });
+ return;
+ }
+
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
if(std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle);
- BoundingBox bb = expolygon.contour.bounding_box();
- // Density adjusted to have a good %of weight.
- double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
- // Distance between the gyroid waves in scaled coordinates.
- coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
+ Polylines polylines;
+ if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
+ // Radians per mm of the regular pattern at a density.
+ auto frequency = [¶ms, 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);
+ } else {
+ BoundingBox bb = expolygon.contour.bounding_box();
+ // Density adjusted to have a good %of weight.
+ double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
+ // Distance between the gyroid waves in scaled coordinates.
+ coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
- // align bounding box to a multiple of our grid module
- bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
+ // align bounding box to a multiple of our grid module
+ bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
- // generate pattern
- Polylines polylines = make_waves(
- scale_(this->z),
- density_adjusted,
- this->spacing,
- ceil(bb.size()(0) / distance) + 1.,
- ceil(bb.size()(1) / distance) + 1.);
+ // generate pattern
+ polylines = make_waves(
+ scale_(this->z),
+ density_adjusted,
+ this->spacing,
+ ceil(bb.size()(0) / distance) + 1.,
+ ceil(bb.size()(1) / distance) + 1.);
+
+ // shift the polyline to the grid origin
+ for (Polyline &pl : polylines)
+ pl.translate(bb.min);
+ }
- // shift the polyline to the grid origin
- for (Polyline &pl : polylines)
- pl.translate(bb.min);
-
// Apply multiline offset if needed
multiline_fill(polylines, params, spacing);
diff --git a/src/libslic3r/Fill/FillTpmsFK.cpp b/src/libslic3r/Fill/FillTpmsFK.cpp
index 164e240280..819ef2998e 100644
--- a/src/libslic3r/Fill/FillTpmsFK.cpp
+++ b/src/libslic3r/Fill/FillTpmsFK.cpp
@@ -8,6 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp"
+#include "FillTpmsAdaptive.hpp"
#include
#include
#include
@@ -17,6 +18,18 @@
#include
#include
#include "libslic3r/Polygon.hpp"
+#include "libslic3r/PrintConfig.hpp"
+
+namespace Slic3r {
+
+// Fischer - Koch S equation:
+// cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
+static float fischer_koch(float x, float y, float z)
+{
+ return cosf(2 * x) * sinf(y) * cosf(z) + cosf(2 * y) * sinf(z) * cosf(x) + cosf(2 * z) * sinf(x) * cosf(y);
+}
+
+} // namespace Slic3r
namespace marchsq {
using namespace Slic3r;
@@ -42,16 +55,7 @@ struct ScalarField
{}
// Get the scalar field value at x,y,z in coordf_t coordinates.
- float get_scalar(coordf_t x, coordf_t y, coordf_t z) const
- {
- const float fx = freq * x;
- const float fy = freq * y;
- const float fz = freq * z;
-
- // Fischer - Koch S equation:
- // cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
- return cosf(2 * fx) * sinf(fy) * cosf(fz) + cosf(2 * fy) * sinf(fz) * cosf(fx) + cosf(2 * fz) * sinf(fx) * cosf(fy);
- }
+ float get_scalar(coordf_t x, coordf_t y, coordf_t z) const { return fischer_koch(freq * x, freq * y, freq * z); }
// Get the scalar field value at a Coord for the current z value.
float get_scalar(Coord p) const
@@ -128,20 +132,34 @@ 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,
+ [&](const FillParams &shell_params, const ExPolygon &shell) {
+ this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
+ });
+ return;
+ }
+
auto infill_angle = float(this->angle + (CorrectionAngle * 2 * M_PI) / 360.);
if (std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle);
- float density_factor = std::min(0.9f, params.density);
// Density (field period) adjusted to have a good %of weight.
- const float vari_T = 4.18f * spacing * params.multiline / density_factor;
+ auto period = [¶ms, this](float density) { return 4.18f * spacing * params.multiline / std::min(0.9f, density); };
BoundingBox bbox = expolygon.contour.bounding_box();
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing));
- marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, vari_T);
- // Get simplified lines using coarse tolerance of 0.1mm (this is infill).
- Polylines polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
+ 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);
+ } else {
+ marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
+ // Get simplified lines using coarse tolerance of 0.1mm (this is infill).
+ polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
+ }
// Apply multiline offset if needed
multiline_fill(polylines, params, spacing);
diff --git a/src/libslic3r/Format/AMF.cpp b/src/libslic3r/Format/AMF.cpp
index fa28d93f09..42583419b4 100644
--- a/src/libslic3r/Format/AMF.cpp
+++ b/src/libslic3r/Format/AMF.cpp
@@ -314,8 +314,13 @@ void AMFParserContext::startElement(const char *name, const char **atts)
case 2:
if (strcmp(name, "metadata") == 0) {
if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) {
- m_value[0] = get_attribute(atts, "type");
- node_type_new = NODE_TYPE_METADATA;
+ const char *type = get_attribute(atts, "type");
+ if (type == nullptr)
+ this->stop();
+ else {
+ m_value[0] = type;
+ node_type_new = NODE_TYPE_METADATA;
+ }
}
}/* else if (strcmp(name, "layer_config_ranges") == 0 && m_path[1] == NODE_TYPE_OBJECT)
node_type_new = NODE_TYPE_LAYER_CONFIG;*/
diff --git a/src/libslic3r/Format/bbs_3mf.cpp b/src/libslic3r/Format/bbs_3mf.cpp
index 61d5c48007..4ae316a055 100644
--- a/src/libslic3r/Format/bbs_3mf.cpp
+++ b/src/libslic3r/Format/bbs_3mf.cpp
@@ -1395,7 +1395,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
bool _handle_start_relationship(const char** attributes, unsigned int num_attributes);
- void _generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
+ bool _generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap& current_objects);
bool _generate_volumes_new(ModelObject& object, const std::vector &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
//bool _generate_volumes(ModelObject& object, const Geometry& geometry, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions);
@@ -2117,7 +2117,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return false;
}
std::vector object_id_list;
- _generate_current_object_list(object_id_list, object.first, m_current_objects);
+ if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
+ return false;
ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
@@ -2216,7 +2217,8 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}*/
std::vector object_id_list;
- _generate_current_object_list(object_id_list, object.first, m_current_objects);
+ if (!_generate_current_object_list(object_id_list, object.first, m_current_objects))
+ return false;
ObjectMetadata::VolumeMetadataList volumes;
ObjectMetadata::VolumeMetadataList* volumes_ptr = nullptr;
@@ -5071,11 +5073,18 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
return true;
}
- void _BBS_3MF_Importer::_generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
+ bool _BBS_3MF_Importer::_generate_current_object_list(std::vector &sub_objects, Id object_id, IdToCurrentObjectMap ¤t_objects)
{
+ // A cycle in the component graph would expand forever, and an acyclic graph can still expand
+ // exponentially, so bound the number of component references queued. Checking before they are
+ // queued bounds the work list itself, whatever the fan-out. A valid file over the budget is
+ // rejected too, but the budget is way above the component references of any real object.
+ static constexpr size_t max_components = 100000;
+
std::list> id_list;
id_list.push_back(std::make_pair(Component(object_id, Transform3d::Identity()), Transform3d::Identity()));
+ size_t num_components = 0;
while (!id_list.empty())
{
auto current_item = id_list.front();
@@ -5085,6 +5094,12 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
if (current_object != current_objects.end()) {
//found one
if (!current_object->second.components.empty()) {
+ num_components += current_object->second.components.size();
+ if (num_components > max_components) {
+ add_error("invalid 3mf: cyclic or too many component references");
+ sub_objects.clear();
+ return false;
+ }
for (const Component &comp : current_object->second.components) {
id_list.push_back(std::pair(comp, current_item.second * comp.transform));
}
@@ -5096,6 +5111,7 @@ void PlateData::parse_filament_info(GCodeProcessorResult *result)
}
}
}
+ return true;
}
bool _BBS_3MF_Importer::_generate_volumes_new(ModelObject& object, const std::vector &sub_objects, const ObjectMetadata::VolumeMetadataList& volumes, ConfigSubstitutionContext& config_substitutions)
diff --git a/src/libslic3r/Layer.cpp b/src/libslic3r/Layer.cpp
index 610dd959d2..20eff0446f 100644
--- a/src/libslic3r/Layer.cpp
+++ b/src/libslic3r/Layer.cpp
@@ -437,10 +437,14 @@ coordf_t Layer::get_sparse_infill_max_void_area()
double max_void_area = 0.;
for (auto layerm : m_regions) {
Flow flow = layerm->flow(frInfill);
- float density = layerm->region().config().sparse_infill_density;
- InfillPattern pattern = layerm->region().config().sparse_infill_pattern;
+ const PrintRegionConfig &config = layerm->region().config();
+ float density = config.sparse_infill_density;
+ InfillPattern pattern = config.sparse_infill_pattern;
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)));
//BBS: rough estimation and need to be optimized
double spacing = flow.scaled_spacing() * (100 - density) / density;
diff --git a/src/libslic3r/Preset.cpp b/src/libslic3r/Preset.cpp
index 2e9577e9c8..5b8127b122 100644
--- a/src/libslic3r/Preset.cpp
+++ b/src/libslic3r/Preset.cpp
@@ -1181,6 +1181,9 @@ static std::vector s_Preset_print_options{
"is_infill_first",
"sparse_infill_density",
"fill_multiline",
+ "tpms_adaptive",
+ "tpms_interior_density",
+ "tpms_adaptive_gradient",
"gyroid_optimized",
"sparse_infill_pattern",
"sparse_infill_smooth_factor",
@@ -2345,7 +2348,7 @@ bool PresetCollection::reset_project_embedded_presets()
return re_select;
}
-void PresetCollection::set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time)
+void PresetCollection::set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id)
{
lock();
const std::string canonical_name = this->canonical_preset_name(name);
@@ -2363,7 +2366,10 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
preset2.save_info();
}
}
- preset->setting_id = setting_id;
+ if (!setting_id.empty())
+ preset->setting_id = setting_id;
+ if (!user_id.empty())
+ preset->user_id = user_id;
if (update_time > 0)
preset->updated_time = update_time;
if (preset->sync_info == "update")
@@ -2653,6 +2659,9 @@ bool PresetCollection::load_user_preset(std::string name, std::mapbase_id = based_id;
iter->filament_id = cloud_filament_id;
update_alias(*iter);
+ // Persist the cloud-assigned identity to disk, mirroring the equal/newer branch
+ // above; otherwise the id stays only in memory and the next launch rewrites it.
+ iter->save_info();
//presets_loaded.emplace_back(*it->second);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", update the user preset %1% from cloud, type %2%, setting_id %3%, base_id %4%, sync_info %5% inherits %6%, filament_id %7%")
% iter->name %Preset::get_type_string(m_type) %iter->setting_id %iter->base_id %iter->sync_info %iter->inherits() % iter->filament_id;
diff --git a/src/libslic3r/Preset.hpp b/src/libslic3r/Preset.hpp
index 2b1fb372af..f06c8f4224 100644
--- a/src/libslic3r/Preset.hpp
+++ b/src/libslic3r/Preset.hpp
@@ -603,7 +603,7 @@ public:
void update_after_user_presets_loaded();
//BBS: get user presets
int get_user_presets(PresetBundle *preset_bundle, std::vector &result_presets);
- void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time);
+ void set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id);
bool need_sync(std::string name, std::string setting_id, long long update_time);
//BBS: add function to generate differed preset for save
diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp
index 6d810a922d..5603716064 100644
--- a/src/libslic3r/Print.cpp
+++ b/src/libslic3r/Print.cpp
@@ -5340,6 +5340,31 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
}
}
+void Print::reload_gcode_moves(GCodeProcessorResult* result) const
+{
+ GCodeProcessor processor;
+ GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
+ const Vec3d origin = this->get_plate_origin();
+ processor.set_xy_offset(origin(0), origin(1));
+ // Estimate the per-move times with the same nozzle-grouping slot context as the export.
+ if (result->nozzle_group_result)
+ processor.initialize_from_context(result->nozzle_group_result);
+ try {
+ processor.process_file(result->filename);
+ } catch (const std::exception& ex) {
+ // The edited file is what gets printed, so failing to preview it must not fail the slice.
+ BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": cannot re-read the G-code file " << result->filename << ": " << ex.what();
+ std::lock_guard lock(result->result_mutex);
+ result->lines_ends.clear();
+ return;
+ }
+
+ GCodeProcessorResult& reloaded = processor.result();
+ std::lock_guard lock(result->result_mutex);
+ result->moves = std::move(reloaded.moves);
+ result->lines_ends = std::move(reloaded.lines_ends);
+}
+
std::tuple Print::object_skirt_offset(double margin_height) const
{
if (config().skirt_loops == 0 || config().skirt_type != stPerObject || m_objects.empty())
diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp
index 062a8eb3a3..45e8703168 100644
--- a/src/libslic3r/Print.hpp
+++ b/src/libslic3r/Print.hpp
@@ -12,6 +12,7 @@
#include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp"
+#include "Fill/FillTpmsAdaptive.hpp"
#include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp"
@@ -406,6 +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(); }
// BBS
void generate_support_preview();
@@ -650,6 +652,7 @@ private:
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data();
+ TpmsRadialFields prepare_tpms_radial_fields() const;
// BBS
SupportNecessaryType is_support_necessary();
@@ -700,6 +703,7 @@ private:
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator;
+ TpmsRadialFields m_tpms_radial_fields;
std::vector < VolumeSlices > firstLayerObjSliceByVolume;
std::vector firstLayerObjSliceByGroups;
@@ -1287,6 +1291,11 @@ public:
void set_gcode_file_ready();
void set_gcode_file_invalidated();
void export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
+ // Re-reads the moves and line offsets of `result` from its G-code file after the file was rewritten in
+ // place (post-processing scripts or plugins), so the preview and its G-code window follow the file on
+ // disk. Everything else in `result` was computed while slicing and is kept. If the file cannot be
+ // re-read, the moves are kept and the line offsets are cleared, which hides the G-code window.
+ void reload_gcode_moves(GCodeProcessorResult* result) const;
//BBS: add modify_count logic
int get_modified_count() const {return m_modified_count;}
//BBS: add status for whether support used
diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp
index ce27b0eb93..cf9b79367e 100644
--- a/src/libslic3r/PrintConfig.cpp
+++ b/src/libslic3r/PrintConfig.cpp
@@ -364,6 +364,26 @@ 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) },
+};
+CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode)
+
+static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{
+ { "linear", int(TpmsAdaptiveGradient::Linear) },
+ { "quadratic", int(TpmsAdaptiveGradient::Quadratic) },
+ { "exponential", int(TpmsAdaptiveGradient::Exponential) },
+};
+CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient)
+
//BBS
static t_config_enum_values s_keys_map_PrintSequence {
{ "by layer", int(PrintSequence::ByLayer) },
@@ -3620,6 +3640,73 @@ 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->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 "
+ "object towards its center. The sparse infill density is used at the surface and the interior "
+ "density at the center.\n"
+ "Distance warp, Smooth blend and Stepped shells follow the distance to the nearest surface, "
+ "including the top and bottom, with the interior density at the point farthest from it:\n"
+ " - Distance warp: one continuous pattern, stretched and sheared where the distance changes "
+ "across directions, as in plates and long parts.\n"
+ " - Smooth blend: the patterns of neighbouring densities blended into each other, with small "
+ "loops where they meet.\n"
+ " - Stepped shells: shells of the regular pattern at densities about 1.5 times apart, their "
+ "lines joined along the shell boundaries.\n"
+ " - Lobes: follows the 3D shape of the object, including its top and bottom. Every lobe, a part "
+ "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::get_enum_values();
+ def->enum_values.push_back("disabled");
+ def->enum_values.push_back("distance_warp");
+ def->enum_values.push_back("smooth_blend");
+ def->enum_values.push_back("stepped_shells");
+ def->enum_values.push_back("lobes");
+ def->enum_values.push_back("normal_z");
+ def->enum_values.push_back("normal_y");
+ def->enum_values.push_back("normal_x");
+ def->enum_labels.push_back(L("Disabled"));
+ def->enum_labels.push_back(L("Distance warp"));
+ def->enum_labels.push_back(L("Smooth blend"));
+ def->enum_labels.push_back(L("Stepped shells"));
+ def->enum_labels.push_back(L("Lobes"));
+ def->enum_labels.push_back(L("Normal Z"));
+ 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::Disabled));
+
+ def = this->add("tpms_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.");
+ def->sidetext = "%";
+ def->min = 1;
+ def->max = 100;
+ def->mode = comAdvanced;
+ def->set_default_value(new ConfigOptionPercent(5));
+
+ def = this->add("tpms_adaptive_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"
+ "Linear: the density changes at a constant rate.\n"
+ "Quadratic: the density stays close to the sparse infill density near the surface and "
+ "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::get_enum_values();
+ def->enum_values.push_back("linear");
+ def->enum_values.push_back("quadratic");
+ def->enum_values.push_back("exponential");
+ def->enum_labels.push_back(L("Linear"));
+ def->enum_labels.push_back(L("Quadratic"));
+ def->enum_labels.push_back(L("Exponential"));
+ def->mode = comAdvanced;
+ def->set_default_value(new ConfigOptionEnum(TpmsAdaptiveGradient::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
// Z-axis compression. Filament use at the same `sparse_infill_density` setting is
diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp
index 7c9633ff78..0e1e146c72 100644
--- a/src/libslic3r/PrintConfig.hpp
+++ b/src/libslic3r/PrintConfig.hpp
@@ -172,6 +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 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
@@ -257,6 +259,26 @@ 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 {
+ Disabled,
+ DistanceWarp,
+ SmoothBlend,
+ SteppedShells,
+ Lobes,
+ NormalZ,
+ NormalY,
+ NormalX,
+ Count,
+};
+
+// Orca: how the adaptive TPMS density changes from the object surface to its deepest point.
+enum class TpmsAdaptiveGradient {
+ Linear,
+ Quadratic,
+ Exponential,
+};
+
//BBS
enum class PrintSequence {
ByLayer,
@@ -774,6 +796,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)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -1450,6 +1474,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline))
+ ((ConfigOptionEnum, tpms_adaptive))
+ ((ConfigOptionPercent, tpms_interior_density))
+ ((ConfigOptionEnum, tpms_adaptive_gradient))
((ConfigOptionBool, gyroid_optimized))
// Ironing options
((ConfigOptionEnum, ironing_type))
diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp
index 3387d22ba8..5ba7882df3 100644
--- a/src/libslic3r/PrintObject.cpp
+++ b/src/libslic3r/PrintObject.cpp
@@ -31,6 +31,7 @@
#include "Fill/FillAdaptive.hpp"
#include "Fill/Fill.hpp"
#include "Fill/FillLightning.hpp"
+#include "Fill/FillTpmsAdaptive.hpp"
#include "format.hpp"
#include "AABBTreeIndirect.hpp"
#include "AABBTreeLines.hpp"
@@ -1297,6 +1298,38 @@ 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
+{
+ TpmsRadialFields fields;
+ std::array 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;
+ if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty())
+ return fields;
+
+ std::vector slices;
+ slices.reserve(m_layers.size());
+ BoundingBox bbox;
+ for (const Layer *layer : m_layers) {
+ slices.push_back({layer->bottom_z(), layer->print_z, &layer->lslices});
+ bbox.merge(get_extents(layer->lslices));
+ }
+ if (!bbox.defined)
+ return fields;
+ for (size_t mode = 0; mode < modes.size(); ++mode) {
+ if (!modes[mode])
+ continue;
+ // Without a field, the infill falls back to the regular pattern.
+ auto field = std::make_unique(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); });
+ if (!field->empty())
+ fields[mode] = std::move(field);
+ }
+ return fields;
+}
+
void PrintObject::clear_layers()
{
if (!m_shared_object) {
@@ -1700,6 +1733,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"
// 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"
@@ -3207,6 +3243,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();
std::vector layers_to_generate_infill;
for (const auto &pair : surfaces_by_layer) {
diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp
index c03607e99c..56551b8e84 100644
--- a/src/libslic3r/TextureBake/TextureBakeDecimate.cpp
+++ b/src/libslic3r/TextureBake/TextureBakeDecimate.cpp
@@ -551,9 +551,13 @@ DecimateResult decimate(const TriSoup &geometry, size_t target_triangles, bool h
// Rebuild from the surviving faces, with per-face normals.
TriSoup &out = result.geometry;
+ if (!face_color.empty())
+ result.face_color.reserve(active_faces);
for (size_t f = 0; f < face_count; ++f) {
if (faces[f * 3] < 0)
continue;
+ if (!face_color.empty())
+ result.face_color.push_back(f < face_color.size() ? face_color[f] : -1);
const Vec3f a = pos[size_t(faces[f * 3])].cast();
const Vec3f b = pos[size_t(faces[f * 3 + 1])].cast();
const Vec3f c = pos[size_t(faces[f * 3 + 2])].cast();
diff --git a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp
index 8b31dbb642..273ad9af35 100644
--- a/src/libslic3r/TextureBake/TextureBakeDecimate.hpp
+++ b/src/libslic3r/TextureBake/TextureBakeDecimate.hpp
@@ -46,6 +46,13 @@ using DecimateProgressFn = std::function;
struct DecimateResult
{
TriSoup geometry;
+ // One entry per output face, carried from the `face_color` handed in: a colour difference is a
+ // crease, so no collapse ever merges two faces of different colour and every survivor keeps exactly
+ // the colour it came with. Empty when no `face_color` was given.
+ //
+ // This is what lets the caller colour the simplified mesh by *provenance* rather than by sampling it
+ // again: the input colours were masked by the paint on the fine mesh, where that mask is exact.
+ std::vector face_color;
// The locked faces alone met the target, so it was unreachable without touching preserved
// geometry.
bool locked_over_budget = false;
diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.cpp b/src/libslic3r/TextureBake/TextureBakeMesh.cpp
index caf6e22d36..0d0a097f98 100644
--- a/src/libslic3r/TextureBake/TextureBakeMesh.cpp
+++ b/src/libslic3r/TextureBake/TextureBakeMesh.cpp
@@ -3,6 +3,7 @@
#include "libslic3r/Point.hpp"
#include
+#include
#include
#include
#include
@@ -42,7 +43,7 @@ TriSoup to_soup(const indexed_triangle_set &its, const std::vector &fac
return out;
}
-indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
+indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector *face_color)
{
indexed_triangle_set out;
const size_t n = soup.pos.size();
@@ -54,12 +55,22 @@ indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup)
if (map.inserted())
out.vertices.push_back(soup.pos[i]);
}
+ const bool track_color = face_color != nullptr && !face_color->empty();
+ std::vector kept_color;
+ if (track_color)
+ kept_color.reserve(face_color->size());
for (size_t t = 0; t + 2 < n; t += 3) {
// Welded-together corners carry no area.
if (id[t] == id[t + 1] || id[t + 1] == id[t + 2] || id[t] == id[t + 2])
continue;
out.indices.emplace_back(id[t], id[t + 1], id[t + 2]);
+ if (track_color) {
+ const size_t src = t / 3;
+ kept_color.push_back(src < face_color->size() ? (*face_color)[src] : -1);
+ }
}
+ if (track_color)
+ *face_color = std::move(kept_color);
return out;
}
diff --git a/src/libslic3r/TextureBake/TextureBakeMesh.hpp b/src/libslic3r/TextureBake/TextureBakeMesh.hpp
index 860aa1445d..7b05ed2451 100644
--- a/src/libslic3r/TextureBake/TextureBakeMesh.hpp
+++ b/src/libslic3r/TextureBake/TextureBakeMesh.hpp
@@ -15,7 +15,10 @@ namespace TextureBake {
TriSoup to_soup(const indexed_triangle_set &its, const std::vector &face_excluded = {});
// Welds at the geometry grid.
-indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup);
+// `face_color`, when given, is read as one entry per soup triangle and rewritten to match the output.
+// Welding can leave a triangle with no area, and those are dropped here, so the two would otherwise
+// fall out of step.
+indexed_triangle_set to_indexed_triangle_set(const TriSoup &soup, std::vector *face_color = nullptr);
} // namespace TextureBake
} // namespace Slic3r
diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.cpp b/src/libslic3r/TextureBake/TextureBakePipeline.cpp
index b44ae837bc..d7f9d9a7b9 100644
--- a/src/libslic3r/TextureBake/TextureBakePipeline.cpp
+++ b/src/libslic3r/TextureBake/TextureBakePipeline.cpp
@@ -290,6 +290,34 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
return result;
}
+ // Colour per face, taken here and carried from here on. This is the only point where the paint mask
+ // is exact: `exclude_weight` says which faces the paint left out, and the mesh is still the refined
+ // one the displacement produced. Everything downstream (the collapse, the T-junction repair) carries
+ // these along rather than sampling again, and the caller uses them as they are.
+ //
+ // It also gives the collapse its crease criterion: an edge between two colours is never collapsed
+ // across, which is what keeps a survivor's colour well defined.
+ if (color_sample) {
+ const size_t nf = displaced.triangle_count();
+ result.face_color.assign(nf, -1);
+ const bool have_w = !displaced.exclude_weight.empty();
+ tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) {
+ for (size_t t = r.begin(); t < r.end(); ++t) {
+ // Unpainted faces take no colour at all, which is what stops the texture appearing on
+ // surfaces the paint never covered.
+ if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
+ displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
+ continue; // stays FACE_UNPAINTED
+ const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
+ const int sampled = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
+ // Painted either way. The sampler expects a point on the base surface and these are on
+ // the displaced one, so off the patch by more than its tolerance it simply says "no
+ // colour" - which must not be confused with "not painted".
+ result.face_color[t] = (sampled >= 0) ? sampled : FACE_NO_COLOUR;
+ }
+ });
+ }
+
// 4. Decimate - export only. A bake needs the face-parent map, which a collapse destroys.
std::vector parent = std::move(sub.face_parent_id);
const size_t displaced_before_decimate = displaced.triangle_count();
@@ -323,24 +351,8 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
// unless the budget was lowered until decimation had to run. Only collapses costing less than
// harvest_tol are taken, so this does not reach the relief.
const bool harvest_only = !over_budget && settings.harvest_flat && displaced.triangle_count() > 0;
+ std::vector &face_color = result.face_color;
if (over_budget || harvest_only) {
- // Colour per face on the fine mesh, so colour boundaries become creases the collapse
- // respects. Excluded (unpainted) faces take no colour.
- std::vector face_color;
- if (color_sample) {
- const size_t nf = displaced.triangle_count();
- face_color.assign(nf, -1);
- const bool have_w = !displaced.exclude_weight.empty();
- tbb::parallel_for(tbb::blocked_range(0, nf), [&](const tbb::blocked_range &r) {
- for (size_t t = r.begin(); t < r.end(); ++t) {
- if (have_w && (displaced.exclude_weight[t * 3] + displaced.exclude_weight[t * 3 + 1] +
- displaced.exclude_weight[t * 3 + 2]) / 3.f > 0.99f)
- continue;
- const Vec3f &a = displaced.pos[t * 3], &b = displaced.pos[t * 3 + 1], &c = displaced.pos[t * 3 + 2];
- face_color[t] = color_sample((a + b + c) / 3.f, displaced.nrm[t * 3]);
- }
- });
- }
// Harvesting alone is asked for by handing it the count it already has: nothing is then
// over the target, so the loop only ever pops collapses under the tolerance.
const size_t before = displaced.triangle_count();
@@ -350,6 +362,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
result.locked_over_budget = dec.locked_over_budget;
result.budget_limited = result.simplified = dec.target_cost_detail;
displaced = std::move(dec.geometry);
+ face_color = std::move(dec.face_color);
lap("decimate", displaced, over_budget ? "over budget, simplified" : "flat faces harvested");
BOOST_LOG_TRIVIAL(info) << "TextureBake decimate: " << before << " -> " << displaced.triangle_count()
<< (over_budget ? " (budget " : " (flat harvest, budget ") << target << ")";
@@ -377,7 +390,7 @@ PipelineResult run_pipeline(const TriSoup &input, const HeightSampleFn &sample,
// 6. Close the T-junctions decimation left behind. Only meaningful when it ran.
if (mode == PipelineMode::Export && parent.empty()) {
- displaced = resolve_t_junctions(displaced);
+ displaced = resolve_t_junctions(displaced, {}, &result.face_color);
lap("repair", displaced);
}
diff --git a/src/libslic3r/TextureBake/TextureBakePipeline.hpp b/src/libslic3r/TextureBake/TextureBakePipeline.hpp
index dbcc46259a..1fc4e4aae9 100644
--- a/src/libslic3r/TextureBake/TextureBakePipeline.hpp
+++ b/src/libslic3r/TextureBake/TextureBakePipeline.hpp
@@ -108,9 +108,27 @@ using PipelineProgressFn = std::function;
+// Sentinels for PipelineResult::face_color.
+static constexpr int FACE_UNPAINTED = -1; // the paint did not cover this face's origin
+static constexpr int FACE_NO_COLOUR = -2; // painted, but the sampler returned nothing at this point
+
struct PipelineResult
{
TriSoup geometry;
+ // One entry per output face, carried through decimation, the T-junction repair and the weld.
+ // FACE_UNPAINTED means the paint never covered the geometry this face came from; anything else means
+ // it did, and is the palette index `color_sample` returned there (FACE_NO_COLOUR when it returned
+ // none). The distinction matters: the sampler answers for points on the *base* surface, and these
+ // are sampled on the displaced one, so a painted face can easily come back without a colour. Only
+ // the painted/unpainted split is reliable here, and that is what a caller should use it for.
+ //
+ // Empty unless the caller gave a `color_sample`.
+ //
+ // A caller that needs per-face colour must use this rather than sampling the result again. The
+ // result is displaced geometry: a point on it is no longer where its base surface was, so matching
+ // it back by proximity colours whatever base surface happens to be nearest - which on a part thinner
+ // than the relief depth is the *opposite* face, picking up the texture meant for the painted one.
+ std::vector face_color;
// Output face -> input face. Empty in Export mode, where decimation invalidates it.
std::vector face_parent_id;
bool safety_cap_hit = false;
diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.cpp b/src/libslic3r/TextureBake/TextureBakeRepair.cpp
index 49f427d461..877379b574 100644
--- a/src/libslic3r/TextureBake/TextureBakeRepair.cpp
+++ b/src/libslic3r/TextureBake/TextureBakeRepair.cpp
@@ -72,7 +72,8 @@ size_t count_area_slivers(const TriSoup &geometry)
return n;
}
-TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
+TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts,
+ std::vector *face_color)
{
const size_t n_tri = geometry.triangle_count();
const double on_tol2 = opts.on_seg_tol * opts.on_seg_tol;
@@ -98,7 +99,12 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
// grid. A needle reads as watertight yet is deleted downstream, and dropping it leaves exactly
// the on-edge-vertex topology the pass below closes.
std::vector> faces;
+ // Parallel to `faces` throughout, so a split or a dropped degenerate keeps the two in step.
+ const bool track_color = face_color != nullptr && !face_color->empty();
+ std::vector colors;
faces.reserve(n_tri);
+ if (track_color)
+ colors.reserve(n_tri);
for (size_t t = 0; t < n_tri; ++t) {
const int a = vid[t * 3], b = vid[t * 3 + 1], c = vid[t * 3 + 2];
if (a == b || b == c || a == c)
@@ -108,6 +114,8 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
if (u.cross(w).squaredNorm() < DEGENERATE_AREA_SQ)
continue;
faces.push_back({ a, b, c });
+ if (track_color)
+ colors.push_back(t < face_color->size() ? (*face_color)[t] : -1);
}
for (int iter = 0; iter < opts.max_iters; ++iter) {
@@ -166,11 +174,16 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
break;
std::vector> next;
+ std::vector next_colors;
next.reserve(faces.size() + splits.size() * 2);
+ if (track_color)
+ next_colors.reserve(next.capacity());
for (size_t fi = 0; fi < faces.size(); ++fi) {
const auto it = splits.find(fi);
if (it == splits.end()) {
next.push_back(faces[fi]);
+ if (track_color)
+ next_colors.push_back(colors[fi]);
continue;
}
const auto &f = faces[fi];
@@ -195,11 +208,18 @@ TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts)
seq.insert(seq.end(), sp.mids.rbegin(), sp.mids.rend());
seq.push_back(sp.a);
}
- for (size_t s = 0; s + 1 < seq.size(); ++s)
+ for (size_t s = 0; s + 1 < seq.size(); ++s) {
next.push_back({ seq[s], seq[s + 1], apex });
+ if (track_color)
+ next_colors.push_back(colors[fi]); // every piece of a split face keeps its colour
+ }
}
faces.swap(next);
+ if (track_color)
+ colors.swap(next_colors);
}
+ if (track_color)
+ *face_color = std::move(colors);
TriSoup out;
out.pos.reserve(faces.size() * 3);
diff --git a/src/libslic3r/TextureBake/TextureBakeRepair.hpp b/src/libslic3r/TextureBake/TextureBakeRepair.hpp
index 07d5323059..ed949dacff 100644
--- a/src/libslic3r/TextureBake/TextureBakeRepair.hpp
+++ b/src/libslic3r/TextureBake/TextureBakeRepair.hpp
@@ -43,7 +43,12 @@ struct RepairOptions
int max_iters = 16;
};
-TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {});
+// `face_color`, when given, is read as one entry per input face and rewritten to match the output: a
+// face split to close a T-junction hands its colour to every piece, and a degenerate face dropped on
+// the way takes its entry with it. Without this the caller would have no way to keep a per-face colour
+// across this pass, which changes the triangle count.
+TriSoup resolve_t_junctions(const TriSoup &geometry, const RepairOptions &opts = {},
+ std::vector *face_color = nullptr);
} // namespace TextureBake
} // namespace Slic3r
diff --git a/src/libslic3r/TextureDisplacement.cpp b/src/libslic3r/TextureDisplacement.cpp
index 5ec7ccdcfb..d0bbb114c6 100644
--- a/src/libslic3r/TextureDisplacement.cpp
+++ b/src/libslic3r/TextureDisplacement.cpp
@@ -2372,12 +2372,11 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
//
// The *palette* index, not the printed filament. The decimation treats any edge whose two faces
// differ as a crease (TextureBakeDecimate.cpp), so it must only ever see where the **perceived**
- // colour changes - which is exactly what ColorResolveFn's own contract says the interleaving may
- // never be fed into. Handing it the resolved filament made every Z band boundary a crease: on an
- // upright wall that is one crease per band, so the collapse ran along those lines and left a stack
- // of horizontal slivers, each printing in a single filament. Those were the horizontal colour
- // lines in the baked result, and they also spent the triangle budget drawing a pattern the eye is
- // meant to blend away. Faces the paint excludes are skipped by the pipeline itself.
+ // colour changes. A mix is one perceived colour however its components are laid down, which is why
+ // it has to be the palette index here: back when this was handed a per-triangle interleave instead,
+ // every band boundary read as a crease, the collapse ran along those lines and left a stack of
+ // horizontal slivers, and the triangle budget went on drawing a pattern the eye is meant to blend
+ // away. Faces the paint excludes are skipped by the pipeline itself.
const TextureBake::ColorSampleFn color_sample =
color_sampler ? TextureBake::ColorSampleFn([&color_sampler](const Vec3f &p, const Vec3f &n) {
return color_sampler(p, n);
@@ -2403,7 +2402,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
stats->triangles_budget = result.triangles_budget;
stats->budget_limited = result.budget_limited;
}
- indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry);
+ indexed_triangle_set out = TextureBake::to_indexed_triangle_set(result.geometry, &result.face_color);
if (out.indices.empty())
return mesh;
if (flip_normals)
@@ -2424,6 +2423,8 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
max_depth = std::max(max_depth, std::abs(layer.depth_mm));
const float relief_tol = max_depth + paint_tol;
std::vector palette(out.indices.size(), -1);
+ const std::vector &face_mask = result.face_color;
+ const bool have_face_mask = face_mask.size() == out.indices.size();
tbb::parallel_for(tbb::blocked_range(0, out.indices.size()), [&](const tbb::blocked_range &r) {
for (size_t i = r.begin(); i < r.end(); ++i) {
const stl_triangle_vertex_indices &t = out.indices[i];
@@ -2439,8 +2440,19 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
// reason; this path was the inconsistent one.
Vec3f foot = centroid, base_n = Vec3f::UnitZ();
const float d2 = painted_closest(centroid, &foot, &base_n);
- if (!all_painted && d2 >= relief_tol * relief_tol)
+ // Which faces may be coloured comes from the pipeline, which recorded it on the
+ // refined mesh where the paint mask is exact, and carried it through the collapse,
+ // the T-junction repair and the weld. Proximity cannot answer this: a displaced face
+ // is no longer where its base was, so on a part thinner than the relief depth the
+ // nearest painted surface to the *opposite* face is the painted one, and the texture
+ // appeared there too. Only the position to sample at still comes from the base
+ // surface, for the projection reason above.
+ if (have_face_mask) {
+ if (face_mask[i] == TextureBake::FACE_UNPAINTED)
+ continue;
+ } else if (!all_painted && d2 >= relief_tol * relief_tol) {
continue;
+ }
palette[i] = sampler(foot, base_n);
}
});
@@ -2462,7 +2474,7 @@ indexed_triangle_set build_texture_displacement_v2(const indexed_triangle_set
Vec3f normal = (b - a).cross(c - a);
const float nl = normal.norm();
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
- const int filament = color->resolve ? color->resolve(palette[i], centroid, normal) : palette[i];
+ const int filament = palette[i];
if (filament >= 0)
out_color[i] = uint8_t(std::min(filament + 1, 255));
}
@@ -2919,8 +2931,7 @@ static indexed_triangle_set build_texture_displacement_in_place(
Vec3f normal = (b - a).cross(c - a);
const float nl = normal.norm();
normal = (nl > 0.f) ? Vec3f(normal / nl) : Vec3f::UnitZ();
- const int filament = color->resolve ? color->resolve(triangle_palette[i], centroid, normal)
- : triangle_palette[i];
+ const int filament = triangle_palette[i];
if (filament >= 0)
out_color[i] = uint8_t(std::min(filament + 1, 255));
}
diff --git a/src/libslic3r/TextureDisplacement.hpp b/src/libslic3r/TextureDisplacement.hpp
index 94d717dea6..34c3543088 100644
--- a/src/libslic3r/TextureDisplacement.hpp
+++ b/src/libslic3r/TextureDisplacement.hpp
@@ -321,25 +321,6 @@ struct TextureDisplacementLayer
}
};
-// How a *mixed* palette entry - one that names two filaments rather than one - is turned into real
-// per-facet paint. An MMU extrudes one filament at a time, so an intermediate colour exists only by
-// interleaving two of them finely enough that the eye does the blending.
-enum class ColorMixMode : int
-{
- // Horizontal bands: which of the two filaments a point takes depends on its height, so
- // consecutive print layers alternate. This is how filament-blend prints actually work, and on a
- // vertical-ish surface it reads as a genuinely smooth colour. On a near-horizontal surface a whole
- // layer is one band, so the blend disappears - that is what XYDither is for.
- ZBands = 0,
- // An ordered (Bayer) checkerboard across the surface, at any orientation. Independent of layer
- // height, but its cell is around the size of one facet, so a fine mix can read as texture rather
- // than as a clean blend.
- XYDither = 1,
- // Per triangle, by its orientation: bands where the surface is upright enough for consecutive
- // layers to alternate, the checkerboard where it faces up or down and a layer would be one band.
- // The default - a flat-topped part with a mix on top gets no blend at all from bands alone.
- Auto = 2,
-};
// Settings that apply to the whole layer stack rather than to one layer, held per ModelVolume next
// to texture_displacement_layers and consumed by build_texture_displacement().
@@ -406,7 +387,6 @@ struct TextureDisplacementOptions
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
// photograph or gradient in mixes. Off forces single filaments everywhere.
bool color_mix_enabled = true;
- ColorMixMode color_mix_mode = ColorMixMode::Auto;
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
@@ -414,11 +394,9 @@ struct TextureDisplacementOptions
template void serialize(Archive &ar)
{
- int mix_mode = int(color_mix_mode);
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
- v2_relocate, color_mix_enabled, mix_mode, color_despeckle);
- color_mix_mode = ColorMixMode(mix_mode);
+ v2_relocate, color_mix_enabled, color_despeckle);
}
};
@@ -511,17 +489,9 @@ DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer
// is. See GLGizmoTextureDisplacement::make_palette_quantizer().
using ColorQuantizeFn = std::function;
-// Resolves a palette index plus a surface position to the filament index that position should print
-// in. A pure entry ignores the position; a mixed one interleaves its two filaments per ColorMixMode.
-//
-// Deliberately separate from ColorQuantizeFn, and deliberately *not* used by the subdivision's colour
-// criterion: that criterion asks where the **perceived** colour changes, and must not see the
-// interleaving. Refining on every band or dither-cell boundary would spend the whole triangle budget
-// drawing a pattern the eye is supposed to blend away.
-using ColorResolveFn = std::function;
// One printable colour: either a loaded filament on its own, or a blend of two of them realised by
-// interleaving (see ColorMixMode). Plain data, so it can be captured into a background job.
+// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
struct PrintableColor
{
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
@@ -538,9 +508,6 @@ struct TextureColorSettings
{
std::vector palette;
std::vector palette_pure; // the filaments alone, for flat-colour images
- ColorMixMode mix_mode = ColorMixMode::ZBands;
- float layer_height = 0.2f; // sizes the Z bands
- float dither_cell_mm = 0.4f; // sizes the XY dither cells
int despeckle_passes = 2;
bool empty() const { return palette.empty(); }
@@ -780,9 +747,6 @@ struct TextureColorRequest
// made of flat colours (TextureDetail::flat_colors) is matched with this one, so a tile or a logo
// prints in single filaments while a photograph on another layer may still use mixes.
ColorQuantizeFn quantize_pure;
- // Palette index + position -> filament. Optional: without it a palette index is taken to be a
- // filament index directly, which is the no-mixing case.
- ColorResolveFn resolve;
// Majority-filter passes over the *perceived* colour, before any interleaving is resolved.
//
// Sampling a detailed image once per triangle leaves salt-and-pepper wherever the image's own
diff --git a/src/slic3r/GUI/ActionRegistry.cpp b/src/slic3r/GUI/ActionRegistry.cpp
index 9d70bdb934..5b1e6f5957 100644
--- a/src/slic3r/GUI/ActionRegistry.cpp
+++ b/src/slic3r/GUI/ActionRegistry.cpp
@@ -149,6 +149,56 @@ std::unique_ptr make_action(const std::string& plugin_key, const std:
return std::make_unique(plugin_key, capability, source_name);
}
+// A plugin page capability exposed as a speed-dial action. source_key = plugin_key
+// (identity), so a plugin display-name change does not re-key the action.
+struct PluginPageAction : AppAction
+{
+ static constexpr const char* kIdPrefix = "plugin_page_action";
+
+ std::string plugin_key;
+ std::string capability;
+
+ // The id an action for (plugin_key, capability) would have - lets refresh_page_capability
+ // remove a gone capability without materialising the action.
+ static std::string id_for(const std::string& plugin_key, const std::string& capability)
+ { return AppAction::compose_id(kIdPrefix, capability.empty() ? plugin_key : capability, plugin_key); }
+
+ PluginPageAction(std::string plugin_key_in, std::string capability_in, std::string source_name)
+ : AppAction(kIdPrefix,
+ capability_in.empty() ? plugin_key_in : capability_in, // title
+ plugin_key_in, // source_key
+ std::move(source_name))
+ , plugin_key(std::move(plugin_key_in))
+ , capability(std::move(capability_in))
+ {
+ // Stay classified as a plugin: grouped under "Plugins" and gated by the same run-confirm.
+ this->kind = AppActionKind::Plugin;
+ // Icon is left empty on purpose: the webview builds resources/images/.svg, which a
+ // plugin filesystem icon path would not resolve to.
+ }
+
+ AppActionRunResult run(const std::string& /*param*/) const override
+ {
+ MainFrame* mf = wxGetApp().mainframe;
+ if (mf)
+ mf->plugin_pages().select_page({PluginCapabilityType::Pages, capability, plugin_key});
+ return {AppActionRunResult::Level::Success};
+ }
+};
+
+// Builds an action for a page capability, or nullptr if it is not a currently-loaded,
+// enabled page capability.
+std::unique_ptr make_page_action(const std::string& plugin_key, const std::string& capability, const std::string& source_name)
+{
+ PluginManager& manager = PluginManager::instance();
+ if (!manager.is_plugin_loaded(plugin_key))
+ return nullptr;
+ // only_enabled defaults true, so a disabled capability resolves to nullptr here.
+ if (!manager.get_plugin_capability({PluginCapabilityType::Pages, capability, plugin_key}))
+ return nullptr;
+ return std::make_unique(plugin_key, capability, source_name);
+}
+
// ---- built-in command actions (the speed dial "commands" section) ------
constexpr const char* kSettingPrefix = "orca_setting";
@@ -311,13 +361,20 @@ void ActionRegistry::init()
});
};
auto on_capability = [this](const PluginCapabilityId& capability, ActionChange change) {
- if (capability.type != PluginCapabilityType::Script || !wxTheApp || wxGetApp().is_closing())
+ if (capability.type != PluginCapabilityType::Script && capability.type != PluginCapabilityType::Pages)
return;
- const std::string plugin_key = capability.plugin_key;
- const std::string name = capability.name;
- wxGetApp().CallAfter([this, plugin_key, name, change] {
- if (!wxGetApp().is_closing())
+ if (!wxTheApp || wxGetApp().is_closing())
+ return;
+ const PluginCapabilityType type = capability.type;
+ const std::string plugin_key = capability.plugin_key;
+ const std::string name = capability.name;
+ wxGetApp().CallAfter([this, type, plugin_key, name, change] {
+ if (wxGetApp().is_closing())
+ return;
+ if (type == PluginCapabilityType::Script)
this->refresh_capability(plugin_key, name, change);
+ else
+ this->refresh_page_capability(plugin_key, name, change);
});
};
@@ -347,6 +404,16 @@ void ActionRegistry::init()
upsert(std::move(action));
}
+ for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Pages)) {
+ if (!capability)
+ continue;
+ const std::string& key = capability->audit_plugin_key();
+ auto it = source_names.find(key);
+ const std::string& source_name = it == source_names.end() ? key : it->second;
+ if (auto action = make_page_action(key, capability->name(), source_name))
+ upsert(std::move(action));
+ }
+
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
// by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in
@@ -401,6 +468,12 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
if (auto action = make_action(plugin_key, capability->name(), source_name))
upsert(std::move(action));
}
+ for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) {
+ if (!capability)
+ continue;
+ if (auto action = make_page_action(plugin_key, capability->name(), source_name))
+ upsert(std::move(action));
+ }
}
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
@@ -420,6 +493,23 @@ void ActionRegistry::refresh_capability(const std::string& plugin_key, const std
remove(id);
}
+void ActionRegistry::refresh_page_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
+{
+ assert(wxThread::IsMain());
+
+ const std::string id = PluginPageAction::id_for(plugin_key, capability);
+ if (change == ActionChange::Removed) {
+ remove(id);
+ return;
+ }
+
+ PluginManager& manager = PluginManager::instance();
+ if (auto action = make_page_action(plugin_key, capability, find_loaded_source_name(manager, plugin_key)))
+ upsert(std::move(action));
+ else
+ remove(id);
+}
+
void ActionRegistry::upsert(std::unique_ptr action)
{
assert(wxThread::IsMain());
diff --git a/src/slic3r/GUI/ActionRegistry.hpp b/src/slic3r/GUI/ActionRegistry.hpp
index 61cdbffd25..4d5800907f 100644
--- a/src/slic3r/GUI/ActionRegistry.hpp
+++ b/src/slic3r/GUI/ActionRegistry.hpp
@@ -239,6 +239,7 @@ private:
// one plugin's whole action set; refresh_capability touches a single capability.
void refresh_source(const std::string& plugin_key, ActionChange change);
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
+ void refresh_page_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
bool m_started = false; // init() runs exactly once; guards double-subscription
std::unordered_map> m_actions; // UI-thread confined; no lock
diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
index 50c69f40dc..ba205887ec 100644
--- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp
+++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp
@@ -283,11 +283,12 @@ void BackgroundSlicingProcess::process_fff()
m_temp_output_path = this->get_current_plate()->get_tmp_gcode_path();
m_fff_print->export_gcode(m_temp_output_path, m_gcode_result,
[this](const ThumbnailsParams& params) { return this->render_thumbnails(params); });
- // Orca: BBL printers post-process the g-code in place here and never re-parse it into a fresh
- // GCodeProcessorResult, so m_gcode_result->nozzle_group_result (consumed by the H2C print-dispatch
- // nozzle mapping) survives post-processing. No preservation guard is needed on this path.
+ // Orca: BBL printers post-process the g-code in place here, in the file the G-code viewer maps, so
+ // the preview re-reads its moves and line offsets from the edited file. The rest of m_gcode_result,
+ // including nozzle_group_result (consumed by the H2C print-dispatch nozzle mapping), is kept.
if (m_fff_print->is_BBL_printer()) {
- run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config());
+ if (run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config()))
+ m_fff_print->reload_gcode_moves(m_gcode_result);
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": export gcode finished");
diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp
index b156f32cb8..cfd985b64b 100644
--- a/src/slic3r/GUI/ConfigManipulation.cpp
+++ b/src/slic3r/GUI/ConfigManipulation.cpp
@@ -843,9 +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;
- // gyroid_optimized only applies when the sparse infill pattern is gyroid;
+ // 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("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("tpms_adaptive") != TpmsAdaptiveMode::Disabled;
+ toggle_line("tpms_interior_density", have_tpms_adaptive);
+ toggle_line("tpms_adaptive_gradient", have_tpms_adaptive);
+
+ // 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);
+ toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_tpms_adaptive);
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
if (have_infill) {
diff --git a/src/slic3r/GUI/GCodeViewer.cpp b/src/slic3r/GUI/GCodeViewer.cpp
index aa4b3ca2bb..03bef82d18 100644
--- a/src/slic3r/GUI/GCodeViewer.cpp
+++ b/src/slic3r/GUI/GCodeViewer.cpp
@@ -895,10 +895,16 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, f
auto update_lines = [this](uint64_t start_id, uint64_t end_id) {
std::vector ret;
ret.reserve(end_id - start_id + 1);
+ // Orca: m_lines_ends indexes into a memory mapping, so it must be clamped to the mapping. If the
+ // file was modified behind our back (an in-place post-processing script that shrank it), an
+ // unchecked read is an access violation, which the caller's try/catch cannot catch on Windows.
+ const size_t file_size = m_file.size();
for (uint64_t id = start_id; id <= end_id; ++id) {
// read line from file
- const size_t start = id == 1 ? 0 : m_lines_ends[id - 2];
- const size_t original_len = m_lines_ends[id - 1] - start;
+ // Keep one entry per id: render() indexes m_lines by (id - start_id).
+ const size_t start = id == 1 ? 0 : std::min(m_lines_ends[id - 2], file_size);
+ const size_t end = std::min(m_lines_ends[id - 1], file_size);
+ const size_t original_len = end > start ? end - start : 0;
// A character is four bytes at most, so 55 of them always fit in 220.
const size_t len = std::min(original_len, (size_t) 55 * 4);
std::string gline(m_file.data() + start, len);
diff --git a/src/slic3r/GUI/GUI_App.cpp b/src/slic3r/GUI/GUI_App.cpp
index 23ae29d8d7..756775a4d9 100644
--- a/src/slic3r/GUI/GUI_App.cpp
+++ b/src/slic3r/GUI/GUI_App.cpp
@@ -7204,11 +7204,11 @@ void GUI_App::sync_preset(Preset* preset, bool force)
BOOST_LOG_TRIVIAL(trace) << "sync_preset: sync operation: " << preset->sync_info << " success! preset = " << preset->name;
if (preset->type == Preset::Type::TYPE_FILAMENT) {
- preset_bundle->filaments.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
+ preset_bundle->filaments.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id());
} else if (preset->type == Preset::Type::TYPE_PRINT) {
- preset_bundle->prints.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
+ preset_bundle->prints.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id());
} else if (preset->type == Preset::Type::TYPE_PRINTER) {
- preset_bundle->printers.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
+ preset_bundle->printers.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id());
}
}
}
@@ -7907,7 +7907,7 @@ void GUI_App::force_push_conflicting_preset(const std::string& setting_id)
? OrcaCloudServiceAgent::generate_uuid_for_setting_id(preset.name, user_id)
: preset.setting_id;
if (preset_id == setting_id) {
- coll->set_sync_info_and_save(preset.name, setting_id, "update", 0);
+ coll->set_sync_info_and_save(preset.name, setting_id, "update", 0, user_id);
break;
}
}
diff --git a/src/slic3r/GUI/GUI_Factories.cpp b/src/slic3r/GUI/GUI_Factories.cpp
index 28c0c1ceb4..5cbda924b5 100644
--- a/src/slic3r/GUI/GUI_Factories.cpp
+++ b/src/slic3r/GUI/GUI_Factories.cpp
@@ -148,6 +148,9 @@ std::map> 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},
{"sparse_infill_smooth_factor", "", 1},
{"lateral_lattice_angle_1", "", 1},
{"lateral_lattice_angle_2", "", 1},
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
index c467a7768d..b6d68383fe 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.cpp
@@ -1514,11 +1514,8 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
// How the entry prints: its filament, or for a mix the two it interleaves and in what ratio.
shader->set_uniform(("palette_a" + idx).c_str(), e.a);
shader->set_uniform(("palette_b" + idx).c_str(), e.b);
- shader->set_uniform(("palette_num" + idx).c_str(), e.num);
- shader->set_uniform(("palette_den" + idx).c_str(), e.den);
}
// The filaments those indices refer to, and the interleave the shader resolves a mix with - the
- // same inputs make_mix_resolver() gets, so the preview shows the pattern that prints rather than
// the mix's smooth average colour. m_palette_filaments is what m_shaded_preview_palette was built from.
const int filament_count =
(palette_count > 0) ? int(std::min(m_palette_filaments.size(), size_t(PALETTE_MAX_FILAMENTS))) : 0;
@@ -1527,9 +1524,6 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
const ColorRGBA &c = m_palette_filaments[size_t(i)];
shader->set_uniform(("filament_rgb[" + std::to_string(i) + "]").c_str(), Vec3f(c.r(), c.g(), c.b()));
}
- shader->set_uniform("mix_mode", int(mv->texture_displacement_options.color_mix_mode));
- shader->set_uniform("layer_height", color_band_mm(*mv)); // as color_settings_for()
- shader->set_uniform("dither_cell", std::max(m_subdivide_color_mm, 0.05f) * 2.f); // as color_settings_for()
if (color_tex != nullptr) {
shader->set_uniform("color_tex", 1);
glsafe(::glActiveTexture(GL_TEXTURE1));
@@ -2074,7 +2068,13 @@ void GLGizmoTextureDisplacement::queue_preview_job()
// The filament list the result's indices refer to, captured with the job rather than read back
// when it lands - loading a filament meanwhile must not recolour a preview computed against a
// different list.
- const std::vector filaments = m_palette_filaments;
+ // Every extruder, not the palette's physical-only list: the bake writes the filament it resolved
+ // to, and a mix resolves to a *mixed filament slot*, which is an extruder past the physical ones.
+ // Grouping against the shorter list dropped every triangle carrying such a slot out of the mesh
+ // entirely - the relief vanished and left only the few triangles that happened to print in a plain
+ // filament. The palette still has to be built from physical filaments alone (see filament_palette()),
+ // which is why these two are not the same list.
+ const std::vector filaments = wxGetApp().plater()->get_extruders_colors();
m_preview_job_running = true;
auto &worker = wxGetApp().plater()->get_ui_job_worker();
@@ -4271,19 +4271,44 @@ bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv)
return false;
}
+void GLGizmoTextureDisplacement::bind_mixes_to_filament_slots(std::vector &palette)
+{
+ Sidebar *sidebar = &wxGetApp().plater()->sidebar();
+ if (sidebar == nullptr)
+ return;
+ for (PaletteEntry &e : palette) {
+ if (!e.is_mix())
+ continue;
+ // Components are 1-based in the config; the ratios are percentages summing to 100, which is the
+ // form create_mixed_filament_from_result() normalises from.
+ const int a_pct = int(std::lround(100.0 * double(e.num) / double(e.den)));
+ const int slot = sidebar->ensure_mixed_filament({ unsigned(e.a + 1), unsigned(e.b + 1) },
+ { a_pct, 100 - a_pct });
+ if (slot >= 0) {
+ e.a = e.b = slot;
+ e.num = e.den = 1;
+ } else {
+ // No room for another slot. Collapse to the component that dominates the blend, which is what
+ // the old per-triangle path did on a surface it could not band anyway.
+ const int dominant = (e.num * 2 >= e.den) ? e.a : e.b;
+ e.a = e.b = dominant;
+ e.num = e.den = 1;
+ }
+ }
+}
+
TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv)
{
TextureColorSettings out;
if (!any_layer_colors(mv))
return out; // nothing is colouring: every colour path stays switched off
out.palette = cached_palette();
- out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false);
- out.mix_mode = mv.texture_displacement_options.color_mix_mode;
+ out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false, PALETTE_MAX_ENTRIES);
+ // Done here rather than in cached_palette(): this runs when a preview or a bake is queued, off a
+ // user action, while that one is also touched from the render path - and creating filament slots
+ // there would mutate the project mid-frame.
+ bind_mixes_to_filament_slots(out.palette);
out.despeckle_passes = mv.texture_displacement_options.color_despeckle;
- out.layer_height = color_band_mm(mv);
- // The dither cell is tied to the colour-detail target: a cell much smaller than a facet cannot be
- // drawn at all, and one much larger stops reading as a blend and starts reading as a check.
- out.dither_cell_mm = std::max(m_subdivide_color_mm, 0.05f) * 2.f;
return out;
}
@@ -4296,10 +4321,15 @@ const std::vector &GLGizmoTextureDispl
const ModelVolume *mv = texture_volume();
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
std::vector filaments = filament_palette();
- if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing) {
+ // Every mix costs a filament slot once they are bound to one, and the mask can name only so many
+ // states, so the palette has to leave room beside the physical filaments it already counts.
+ const int cap = int(EnforcerBlockerType::ExtruderMax);
+ if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing ||
+ cap != m_palette_cap) {
m_palette_filaments = std::move(filaments);
m_palette_mixing = mixing;
- m_palette_cache = make_palette(m_palette_filaments, mixing);
+ m_palette_cap = cap;
+ m_palette_cache = make_palette(m_palette_filaments, mixing, cap);
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
}
return m_palette_cache;
@@ -4307,40 +4337,29 @@ const std::vector &GLGizmoTextureDispl
std::vector GLGizmoTextureDisplacement::filament_palette()
{
- std::vector palette = wxGetApp().plater()->get_extruders_colors();
- // mmu_segmentation_facets encodes the filament in a 6-bit prefix code and stops at Extruder16.
+ std::vector all = wxGetApp().plater()->get_extruders_colors();
+
+ // Physical filaments only. The mixes this palette produces each become a mixed filament slot of
+ // their own (see bind_mixes_to_filament_slots()), and those slots are extruders too - so taking the
+ // list as it comes meant the next rebuild mixed *them* again, and handed components naming a
+ // virtual slot to a blend that can only name physical ones. That is what left entries reading
+ // "filament 1 plus nothing" and raised "Mixed filament has invalid or mismatched components".
+ const auto *is_mixed = wxGetApp().preset_bundle->project_config.option("filament_is_mixed");
+ std::vector palette;
+ palette.reserve(all.size());
+ for (size_t i = 0; i < all.size(); ++i)
+ if (is_mixed == nullptr || i >= is_mixed->values.size() || !is_mixed->values[i])
+ palette.push_back(all[i]);
+
+ // A paint mask can only name so many states, and every mix spends one beside these.
if (palette.size() > size_t(EnforcerBlockerType::ExtruderMax))
palette.resize(size_t(EnforcerBlockerType::ExtruderMax));
return palette;
}
-float GLGizmoTextureDisplacement::color_band_mm(const ModelVolume &mv)
-{
- const float lh = print_layer_height();
- const float edge = (mv.texture_displacement_options.v2_refine_mm > 0.f) ? mv.texture_displacement_options.v2_refine_mm
- : v2_recommendation(mv).edge_mm;
- if (edge <= 0.f || lh <= 0.f)
- return lh;
- // A refined triangle of edge e stacks in rows about 0.87 * e apart (an equilateral triangle's
- // height), and a dither needs at least two rows per period to be a dither at all.
- constexpr float ROW_PER_EDGE = 0.87f;
- return lh * std::max(1.f, std::ceil(2.f * ROW_PER_EDGE * edge / lh));
-}
-
-float GLGizmoTextureDisplacement::print_layer_height()
-{
- try {
- const DynamicPrintConfig &cfg = wxGetApp().preset_bundle->prints.get_edited_preset().config;
- if (const ConfigOptionFloat *opt = cfg.option("layer_height"); opt != nullptr)
- if (opt->value > 1e-3)
- return float(opt->value);
- } catch (...) {
- }
- return 0.2f;
-}
std::vector GLGizmoTextureDisplacement::make_palette(
- const std::vector &filaments, bool mixing)
+ const std::vector &filaments, bool mixing, int max_entries)
{
std::vector out;
const int n = int(filaments.size());
@@ -4357,7 +4376,7 @@ std::vector GLGizmoTextureDisplacement
const int pairs = n * (n - 1) / 2;
int steps = 0;
for (int s = 5; s >= 1; --s)
- if (n + pairs * s <= PALETTE_MAX_ENTRIES) {
+ if (n + pairs * s <= max_entries) {
steps = s;
break;
}
@@ -4379,51 +4398,6 @@ std::vector GLGizmoTextureDisplacement
return out;
}
-ColorResolveFn GLGizmoTextureDisplacement::make_mix_resolver(const std::vector &palette,
- ColorMixMode mode, float layer_height,
- float cell_mm)
-{
- if (palette.empty())
- return nullptr;
- auto entries = std::make_shared>(palette);
- const float band = std::max(layer_height, 0.01f);
- const float cell = std::max(cell_mm, 0.01f);
-
- return [entries, mode, band, cell](int index, const Vec3f &pos, const Vec3f &normal) -> int {
- if (index < 0 || size_t(index) >= entries->size())
- return -1;
- const PaletteEntry &e = (*entries)[size_t(index)];
- if (!e.is_mix())
- return e.a;
-
- // Which of the two filaments this point falls on. Both patterns are *ordered*, never random:
- // the eye blends a regular pattern into a flat colour, and turns a random one into noise.
- // Auto: bands wherever the surface is steeper than ~45 degrees - consecutive layers alternate
- // there, which is how a blend prints and reads. On a flat-facing surface a layer is one band
- // and the only way to interleave is a checkerboard across the surface, which at print scale
- // reads as a pattern rather than a colour; there the mix falls back to its dominant filament.
- const bool upright = std::abs(normal.z()) < 0.7f;
- if (mode == ColorMixMode::Auto && !upright)
- return e.num * 2 >= e.den ? e.a : e.b;
- const bool bands = mode == ColorMixMode::ZBands || mode == ColorMixMode::Auto;
- if (bands) {
- // One band per print layer. floorf, not a cast, so this stays correct below z = 0.
- const int slot = int(std::floor(pos.z() / band));
- const int phase = ((slot % e.den) + e.den) % e.den;
- return phase < e.num ? e.a : e.b;
- }
- // Ordered 4x4 Bayer over the surface, indexed by position so the pattern is stable in space
- // rather than in triangle order (which would move under any remesh, and read as noise).
- static const int BAYER[16] = { 0, 8, 2, 10, 12, 4, 14, 6, 3, 11, 1, 9, 15, 7, 13, 5 };
- const int gx = ((int(std::floor(pos.x() / cell)) % 4) + 4) % 4;
- const int gy = ((int(std::floor(pos.y() / cell)) % 4) + 4) % 4;
- // A third axis would be ideal, but the two dominant ones are enough for a surface pattern and
- // keep the cell square on the faces that matter.
- const float threshold = (float(BAYER[gy * 4 + gx]) + 0.5f) / 16.f;
- return (float(e.num) / float(e.den)) > threshold ? e.a : e.b;
- };
-}
-
ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::vector &palette)
{
if (palette.empty())
@@ -4460,10 +4434,15 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
best_pure = int(i);
}
}
- // A mix is an interleave that only reads as its colour from a distance; up close
- // it is stripes. Spend it only where it buys a clearly better match than the nearest
- // single filament: ten Delta E is a visible step, less is not worth the stripes.
- constexpr float PREFER_PURE_DE = 10.f;
+ // A mix is an interleave that only reads as its colour from a distance; up close it is
+ // stripes. So it is spent only where it buys a better match than the nearest single
+ // filament - but "better" was set at ten Delta E, which is not a visible step, it is a
+ // different colour. Measured over the whole cube that threshold turned 94% of the
+ // lookups that wanted a mix back into a pure filament, leaving 38%; along a greyscale
+ // ramp, the shape a height texture actually traces, it cut 80% to 66%. Two Delta E is
+ // about where a side-by-side difference stops being arguable, which is the right place
+ // to start paying for stripes.
+ constexpr float PREFER_PURE_DE = 2.f;
if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE)
best = best_pure;
(*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best);
@@ -4563,7 +4542,7 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
if (params.subdiv_color_edge_mm > 0.f && !palette.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(palette));
// Note the sampler is built on the *quantizer* alone - the refinement follows perceived
- // colour, never the interleaving that realises a mix (see ColorResolveFn).
+ // colour, never the interleaving that realises a mix - the slicer does that per layer.
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
// to); in plain adaptive mode the target edge length is the only criterion, so the floor
// must not be allowed to silently override a target the user set below it.
@@ -6148,24 +6127,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"of filaments can cover a photo or a gradient. An image of flat colors "
"prints the same either way. Off uses one filament per area."));
if (opts.color_mix_enabled) {
- slider_label(_L("Mix by"));
- const std::string mix_z = _u8L("Layers");
- const std::string mix_xy = _u8L_CONTEXT("Surface", "Texture Displacement");
- const std::string mix_auto = _u8L("Automatic");
- const char *mix_items[] = { mix_z.c_str(), mix_xy.c_str(), mix_auto.c_str() };
- int mix_mode = int(opts.color_mix_mode);
- ImGui::SetNextItemWidth(-card_pad);
- if (scoped_combo("##color_mix_mode", &mix_mode, mix_items, IM_ARRAYSIZE(mix_items))) {
- opts.color_mix_mode = ColorMixMode(mix_mode);
- m_preview_params_dirty = true;
- }
- hover_tip(_u8L("Layers: the two filaments alternate between print layers, which "
- "blends smoothly on upright surfaces but disappears on flat-facing "
- "ones, where a whole layer is a single band.\n"
- "Surface: a fine checkerboard across the surface, which works at "
- "any angle but can read as texture rather than as a blend.\n"
- "Automatic: layers on upright faces; flat-facing faces take the nearer "
- "single filament, since a checkerboard there shows as a pattern."));
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
diff --git a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
index 2c6f5bc6c7..2406ddbe84 100644
--- a/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
+++ b/src/slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp
@@ -81,7 +81,11 @@ public:
// How many ratios depends on how many filaments there are, so the palette stays bounded: the
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
// filaments there are already plenty of colours without mixing any of them.
- static std::vector make_palette(const std::vector &filaments, bool mixing);
+ // `max_entries` bounds the whole palette. It is normally the quantizer's own limit, but when the
+ // mixes become filament slots it has to be the paint mask's instead: a mask can name only
+ // EnforcerBlockerType::ExtruderMax states, and every mix now occupies one of them.
+ static std::vector make_palette(const std::vector &filaments, bool mixing,
+ int max_entries);
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
@@ -95,9 +99,6 @@ public:
// Turns a palette index plus a position into the filament to print there, interleaving the two
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither
- // cells. See ColorResolveFn for why this is separate from the quantizer.
- static ColorResolveFn make_mix_resolver(const std::vector &palette, ColorMixMode mode,
- float layer_height, float cell_mm);
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
// height, despeckle. Empty when no layer is actually colouring.
@@ -106,16 +107,20 @@ public:
// The printable palette for the current filaments and mixing setting, rebuilt only when either
// actually changes - see the definition for why that caching is not optional.
const std::vector &cached_palette();
+ // Turns every mix in `palette` into a mixed filament slot and rewrites the entry to name that slot
+ // as a plain filament, so nothing downstream has to know a mix is involved: is_mix() goes false and
+ // the resolver simply returns it. The per-layer interleaving then happens in the slicer, where it is
+ // not limited by how fine the mesh is. Entries whose slot could not be created (the paint-state cap)
+ // fall back to the nearer of the two components.
+ void bind_mixes_to_filament_slots(std::vector &palette);
std::vector m_palette_cache;
std::vector m_palette_filaments;
+ int m_palette_cap = 0; // the max_entries m_palette_cache was built with
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer;
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
static std::vector filament_palette();
- // The print's layer height, which sizes ColorMixMode::ZBands. Falls back to 0.2 mm if it cannot be
- // read - a wrong band size is a cosmetic error, not a reason to refuse to colour anything.
- static float print_layer_height();
// The Z band height, in mm. One print layer is the ideal, but the interleave is realised per
// *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle
// grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour,
@@ -123,7 +128,6 @@ public:
// diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of
// layers at least two facet rows tall: still exact on the printer, and representable by the mesh
// that has to carry it. Used by both the bake settings and the preview shader, so the two agree.
- float color_band_mm(const ModelVolume &mv);
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer
diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
index 71c4f4afe7..b876acb62e 100644
--- a/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
+++ b/src/slic3r/GUI/Jobs/TextureDisplacementBakeJob.cpp
@@ -53,9 +53,6 @@ void TextureDisplacementBakeJob::process(Ctl &ctl)
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
if (!m_input.color.palette_pure.empty())
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
- color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
- m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
- m_input.color.dither_cell_mm);
color_request.despeckle_passes = m_input.color.despeckle_passes;
color_request.out_triangle = &m_triangle_color;
if (color_request.quantize)
diff --git a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp
index 49cbae4e81..4243cdd9d3 100644
--- a/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp
+++ b/src/slic3r/GUI/Jobs/TextureDisplacementPreviewJob.cpp
@@ -32,9 +32,6 @@ void TextureDisplacementPreviewJob::process(Ctl &ctl)
color_request.quantize = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette);
if (!m_input.color.palette_pure.empty())
color_request.quantize_pure = GLGizmoTextureDisplacement::make_palette_quantizer(m_input.color.palette_pure);
- color_request.resolve = GLGizmoTextureDisplacement::make_mix_resolver(
- m_input.color.palette, m_input.color.mix_mode, m_input.color.layer_height,
- m_input.color.dither_cell_mm);
color_request.despeckle_passes = m_input.color.despeckle_passes;
color_request.out_triangle = &m_result.triangle_color;
if (color_request.quantize)
diff --git a/src/slic3r/GUI/Plater.cpp b/src/slic3r/GUI/Plater.cpp
index a666e23c9c..51a0c85909 100644
--- a/src/slic3r/GUI/Plater.cpp
+++ b/src/slic3r/GUI/Plater.cpp
@@ -937,6 +937,7 @@ struct Sidebar::priv
StaticLine* m_text_mixed_title{nullptr};
ScalableButton* m_btn_mixed_add{nullptr};
ScalableButton* m_btn_mixed_del{nullptr};
+ ScalableButton* m_btn_mixed_del_all{nullptr};
wxScrolledWindow* m_mixed_scroll_area{nullptr}; // independent scrollbar for mixed rows
wxPanel* m_panel_mixed_content{nullptr};
wxBoxSizer* m_sizer_mixed_filaments{nullptr}; // two-column, mirrors sizer_filaments
@@ -3451,6 +3452,11 @@ Sidebar::Sidebar(Plater *parent)
});
title_sizer->Add(p->m_btn_mixed_del, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing()));
+ p->m_btn_mixed_del_all = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "delete_all_filaments");
+ p->m_btn_mixed_del_all->SetToolTip(_L("Remove all mixed filaments"));
+ p->m_btn_mixed_del_all->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { remove_all_mixed_filaments(); });
+ title_sizer->Add(p->m_btn_mixed_del_all, 0, wxALIGN_CENTER_VERTICAL | wxLEFT, FromDIP(SidebarProps::IconSpacing()));
+
p->m_btn_mixed_add = new ScalableButton(p->m_panel_mixed_title, wxID_ANY, "add_filament");
p->m_btn_mixed_add->SetToolTip(_L("Add mixed filament"));
p->m_btn_mixed_add->Bind(wxEVT_BUTTON, [this](wxCommandEvent&) { add_mixed_filament(); });
@@ -5124,6 +5130,58 @@ static bool create_mixed_filament_from_result(
return true;
}
+int Sidebar::ensure_mixed_filament(const std::vector &components, const std::vector &ratios)
+{
+ if (components.size() < 2 || components.size() != ratios.size())
+ return -1;
+ if (p->combos_filament.size() < 2)
+ return -1;
+
+ // Normalise the way create_mixed_filament_from_result() stores them, so the comparison below sees
+ // the same text the config holds rather than two spellings of one blend.
+ int ratio_sum = 0;
+ for (const int r : ratios)
+ ratio_sum += r;
+ if (ratio_sum <= 0)
+ return -1;
+
+ std::string comp_str, ratio_str;
+ {
+ CNumericLocalesSetter c_locale_setter;
+ for (size_t i = 0; i < components.size(); ++i) {
+ if (i > 0) { comp_str += ","; ratio_str += ","; }
+ comp_str += std::to_string(components[i]);
+ char buf[32];
+ std::snprintf(buf, sizeof(buf), "%.4f", float(ratios[i]) / float(ratio_sum));
+ ratio_str += buf;
+ }
+ }
+
+ const auto &project_config = wxGetApp().preset_bundle->project_config;
+ const auto *is_mixed_opt = project_config.option("filament_is_mixed");
+ const auto *comp_opt = project_config.option("filament_mixed_components");
+ const auto *ratios_opt = project_config.option("filament_mixed_sublayer_ratios");
+ if (is_mixed_opt != nullptr && comp_opt != nullptr && ratios_opt != nullptr)
+ for (size_t i = 0; i < is_mixed_opt->values.size(); ++i)
+ if (is_mixed_opt->values[i] && i < comp_opt->values.size() && i < ratios_opt->values.size() &&
+ comp_opt->values[i] == comp_str && ratios_opt->values[i] == ratio_str)
+ return int(i);
+
+ if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax))
+ return -1;
+
+ std::vector color_strs, names, types;
+ collect_physical_filament_info(color_strs, names, types);
+
+ MixedFilamentResult result;
+ result.components = components;
+ result.ratios = ratios;
+ const size_t created_at = wxGetApp().preset_bundle->filament_presets.size();
+ if (!create_mixed_filament_from_result(this, result, color_strs))
+ return -1;
+ return int(created_at);
+}
+
void Sidebar::add_mixed_filament()
{
auto* plater = dynamic_cast(GetParent());
@@ -5301,6 +5359,28 @@ void Sidebar::edit_mixed_filament(size_t panel_idx)
}
}
+void Sidebar::remove_all_mixed_filaments()
+{
+ auto *plater = dynamic_cast(GetParent());
+ if (plater == nullptr)
+ return;
+ const size_t count = plater->mixed_filament_config_indices().size();
+ if (count == 0)
+ return;
+
+ // Worth a confirmation: this drops filament slots the model may be painted with, and anything
+ // painted in one falls back to a plain filament.
+ MessageDialog dlg(this, format_wxstr(_L("Remove all %1% mixed filaments?"), count), _L("Mixed Filament"),
+ wxYES_NO | wxNO_DEFAULT | wxICON_QUESTION);
+ if (dlg.ShowModal() != wxID_YES)
+ return;
+
+ // Back to front: delete_mixed_filament_at() indexes the list as it stands, so removing from the end
+ // leaves the indices of everything still to go untouched.
+ for (size_t i = count; i-- > 0;)
+ delete_mixed_filament_at(i);
+}
+
void Sidebar::delete_mixed_filament_at(size_t panel_idx)
{
auto* plater = dynamic_cast(GetParent());
@@ -14996,7 +15076,8 @@ bool Plater::priv::undo_redo_blocked_by_job()
return false;
notification_manager->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel,
- _u8L("Cannot undo or redo while an operation is running. Stop it first."));
+ _u8L("Cannot undo or redo while an operation is running. Stop the operation, or wait "
+ "for it to finish and then retry."));
return true;
}
diff --git a/src/slic3r/GUI/Plater.hpp b/src/slic3r/GUI/Plater.hpp
index c3b4523c49..46192794c6 100644
--- a/src/slic3r/GUI/Plater.hpp
+++ b/src/slic3r/GUI/Plater.hpp
@@ -291,8 +291,19 @@ public:
// Mixed-color filament sidebar section
void add_mixed_filament();
+ // The filament slot that blends `components` (1-based physical filament indices) in `ratios`
+ // (percentages), creating it when no existing mixed slot already describes that blend. Returns the
+ // 0-based filament index, or -1 when the paint-state cap leaves no room for another one.
+ //
+ // Exists so a feature that needs a blend can ask for one without going through the modal dialog:
+ // the texture displacement gizmo turns each mix in its palette into a slot, which is what moves the
+ // interleaving from its own paint mask to the slicer, where it happens per layer.
+ int ensure_mixed_filament(const std::vector &components, const std::vector &ratios);
void edit_mixed_filament(size_t idx);
void delete_mixed_filament_at(size_t idx);
+ // Drops every mixed filament at once, after confirming. The texture displacement gizmo can create
+ // one slot per colour in its palette, so clearing them one at a time is tedious.
+ void remove_all_mixed_filaments();
void decompose_filament_color(int filament_idx);
void recalc_filament_scroll_sizes();
void update_mixed_filament_list();
diff --git a/src/slic3r/GUI/Tab.cpp b/src/slic3r/GUI/Tab.cpp
index 49457e8bc8..7c9f486426 100644
--- a/src/slic3r/GUI/Tab.cpp
+++ b/src/slic3r/GUI/Tab.cpp
@@ -2961,6 +2961,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("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");
diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp
index c3e67afef1..7aa2b1ab2e 100644
--- a/src/slic3r/Utils/OrcaCloudServiceAgent.cpp
+++ b/src/slic3r/Utils/OrcaCloudServiceAgent.cpp
@@ -1129,6 +1129,19 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name,
if (http_code)
*http_code = result.http_code;
+ // 409 duplicate_profile_uuid in the create path means the deterministic id we
+ // just generated already exists in this account: the earlier create succeeded.
+ // Adopt it instead of failing, so sync_preset persists the id and stops retrying.
+ if (result.http_code == 409 && result.conflict_code == -2
+ && !result.server_version.id.empty() && result.server_version.id == new_id) {
+ if (values_map && result.server_version.updated_time != 0)
+ (*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.server_version.updated_time);
+ if (http_code)
+ *http_code = 200;
+ BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: request_setting_id adopted existing profile id " << new_id << " (409 duplicate_profile_uuid)";
+ return new_id;
+ }
+
if (result.success) {
if (values_map && result.new_updated_time != 0) {
(*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.new_updated_time);
@@ -1394,6 +1407,7 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
SyncPushResult result;
result.success = false;
result.http_code = 0;
+ result.conflict_code = 0;
result.server_deleted = false;
nlohmann::json body;
@@ -1429,20 +1443,30 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
err_body = json;
if (json.is_null()) {
result.server_deleted = true;
- } else {
- auto& profile_data = json["server_profile"];
- result.server_version.id = profile_data.value("id", "");
- result.server_version.name = profile_data.value("name", "");
- result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
+ } else if (json.is_object()) {
+ result.conflict_code = json.value("code", 0);
+ if (json.contains("server_profile") && !json["server_profile"].is_null()) {
+ auto& profile_data = json["server_profile"];
+ result.server_version.id = profile_data.value("id", "");
+ result.server_version.name = profile_data.value("name", "");
+ result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
+ }
}
} catch (...) {}
- // Surface the conflict via the http-error callback with the local preset name injected.
- // The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
- // but the GUI needs it to regenerate the deterministic setting_id for a force push.
- if (!err_body.is_object())
- err_body = nlohmann::json::object();
- err_body["name"] = name;
- invoke_http_error_callback(409, err_body.dump());
+ // Create-path duplicate_profile_uuid (-2) is an idempotent success: the deterministic id
+ // already exists, so the caller adopts the returned id. Skip the conflict notification,
+ // otherwise every already-imported preset would raise a Pull/Force-push prompt on each launch.
+ const bool is_create = original_updated_time.empty();
+ const bool auto_resolved_duplicate = (is_create && result.conflict_code == -2);
+ if (!auto_resolved_duplicate) {
+ // Surface the conflict via the http-error callback with the local preset name injected.
+ // The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
+ // but the GUI needs it to regenerate the deterministic setting_id for a force push.
+ if (!err_body.is_object())
+ err_body = nlohmann::json::object();
+ err_body["name"] = name;
+ invoke_http_error_callback(409, err_body.dump());
+ }
result.error_message = response;
return result;
}
diff --git a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp
index 458aa55fcd..d4a182db12 100644
--- a/src/slic3r/Utils/OrcaCloudServiceAgent.hpp
+++ b/src/slic3r/Utils/OrcaCloudServiceAgent.hpp
@@ -94,6 +94,7 @@ struct SyncPullResponse {
struct SyncPushResult {
bool success;
int http_code;
+ int conflict_code;
long long new_updated_time;
ProfileUpsert server_version;
bool server_deleted;
diff --git a/src/slic3r/plugin/host/PluginPages.cpp b/src/slic3r/plugin/host/PluginPages.cpp
index f5d0bf8f91..cf4ed6a6a8 100644
--- a/src/slic3r/plugin/host/PluginPages.cpp
+++ b/src/slic3r/plugin/host/PluginPages.cpp
@@ -400,6 +400,17 @@ void PluginPages::relayout()
}
}
+void PluginPages::select_page(const PluginCapabilityId& id)
+{
+ if (m_parent == nullptr || m_pages.find(id) == m_pages.end())
+ return;
+
+ // Swap the page into the visible slot first when it lives behind the overflow menu.
+ m_swapped_in_id = id;
+ relayout();
+ m_parent->SelectPageByName(page_tab_id(id));
+}
+
void PluginPages::show_overflow_menu()
{
const int visible_slots = std::max(1, m_visible_page_count);
diff --git a/src/slic3r/plugin/host/PluginPages.hpp b/src/slic3r/plugin/host/PluginPages.hpp
index 962e02ecd0..61c64f53d5 100644
--- a/src/slic3r/plugin/host/PluginPages.hpp
+++ b/src/slic3r/plugin/host/PluginPages.hpp
@@ -66,6 +66,8 @@ public:
void relayout();
+ void select_page(const PluginCapabilityId& id);
+
private:
std::shared_ptr get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const;
bool create_page(const PluginCapabilityId& id);
diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp
index e1d93c5e15..1fbedcda8f 100644
--- a/tests/fff_print/test_fill.cpp
+++ b/tests/fff_print/test_fill.cpp
@@ -37,6 +37,7 @@
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Fill/FillGyroid.hpp"
+#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp"
@@ -2021,3 +2022,334 @@ TEST_CASE("Adaptive infill of a modifier leaves the density of the other regions
CHECK(unmatched_between_prints(print, print_sparse, erInternalInfill, {rect(3, 3, 27, 27), rect(43, 3, 52, 27)}) < 0.02);
CHECK(unmatched_between_prints(print, print_dense, erInternalInfill, {rect(58, 3, 67, 27)}) < 0.02);
}
+
+// Length of the sparse infill of the first object over the layers with print_z in [z_min, z_max], inside clip.
+static double sparse_infill_length(const Print &print, coordf_t z_min, coordf_t z_max, const Polygons &clip)
+{
+ Polylines polylines;
+ for (const Layer *layer : print.objects().front()->layers())
+ if (layer->print_z >= z_min && layer->print_z <= z_max)
+ for (const LayerRegion *region : layer->regions())
+ for (const ExtrusionEntity *entity : region->fills.flatten().entities)
+ if (entity->role() == erInternalInfill)
+ entity->collect_polylines(polylines);
+ return unscale(total_length(intersection_pl(polylines, clip)));
+}
+
+static Polygons centered_square(double half)
+{
+ return {Polygon({Point::new_scale(-half, -half), Point::new_scale(half, -half), Point::new_scale(half, half), Point::new_scale(-half, half)})};
+}
+
+TEST_CASE("Adaptive TPMS infill thins out from the surface to the interior density", "[Fill]")
+{
+ // The modes following the distance print more lines than their target where its levels meet: along the shells, or
+ // where the patterns blend.
+ const auto row = GENERATE(table({{"tpmsd", "lobes", 0.5},
+ {"tpmsfk", "lobes", 0.5},
+ {"gyroid", "lobes", 0.5},
+ {"gyroid", "stepped_shells", 0.6},
+ {"tpmsfk", "smooth_blend", 0.6},
+ {"tpmsd", "distance_warp", 0.5}}));
+ const std::string pattern = std::get<0>(row);
+ const std::string mode = std::get<1>(row);
+ const double max_core = std::get<2>(row);
+ CAPTURE(pattern, mode);
+ // A 60 mm cube in 0.4 mm layers, centered on the origin in XY. Its center is 30 mm from every face.
+ auto slice = [&pattern](const std::string &adaptive, Print &print) {
+ 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"},
+ {"layer_height", 0.4},
+ {"initial_layer_print_height", 0.4}});
+ };
+ Print uniform, adaptive;
+ slice("disabled", uniform);
+ slice(mode, adaptive);
+
+ // The middle of the cube, at most 10 mm from the center: at most 5% + 20% * 10 / 30 = 11.7% dense.
+ const Polygons core = centered_square(10.);
+ // Along the sides at mid height, 24 to 28 mm from the center: at least 5% + 20% * 24 / 30 = 21% dense.
+ const Polygons shell = diff(centered_square(28.), centered_square(24.));
+ const double core_uniform = sparse_infill_length(uniform, 25., 35., core);
+ const double shell_uniform = sparse_infill_length(uniform, 25., 35., shell);
+ REQUIRE(core_uniform > 0.);
+ REQUIRE(shell_uniform > 0.);
+ CHECK(sparse_infill_length(adaptive, 25., 35., core) < max_core * core_uniform);
+ CHECK(sparse_infill_length(adaptive, 25., 35., shell) > 0.75 * shell_uniform);
+}
+
+TEST_CASE("Adaptive TPMS infill of a tall object is sparsest at its middle height", "[Fill]")
+{
+ // A 30 x 30 x 90 mm box in 0.4 mm layers: its center is 45 mm high, the middle of its core, not a column.
+ Print print;
+ 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"},
+ {"layer_height", 0.4},
+ {"initial_layer_print_height", 0.4}});
+ const Polygons core = centered_square(5.);
+ const double middle = sparse_infill_length(print, 40., 50., core);
+ const double low = sparse_infill_length(print, 10., 20., core);
+ REQUIRE(low > 0.);
+ CHECK(middle < 0.7 * low);
+}
+
+TEST_CASE("2D adaptive TPMS infill does not change along its axis", "[Fill]")
+{
+ // A box of 30 x 30 mm sections, 90 mm long along the axis, centered on the origin in XY.
+ const std::string mode = GENERATE("normal_x", "normal_z");
+ CAPTURE(mode);
+ const bool along_x = mode == "normal_x";
+ auto slice = [&](const std::string &adaptive, Print &print) {
+ 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"},
+ {"layer_height", 0.4},
+ {"initial_layer_print_height", 0.4}});
+ };
+ Print uniform, adaptive;
+ slice("disabled", uniform);
+ slice(mode, adaptive);
+
+ // The core of the sections near an end and at the middle of the box, at most 5 mm from their center.
+ auto rectangle = [](double x0, double x1) {
+ return Polygons{Polygon({Point::new_scale(x0, -5.), Point::new_scale(x1, -5.), Point::new_scale(x1, 5.), Point::new_scale(x0, 5.)})};
+ };
+ auto core = [&](const Print &print, bool end) {
+ return along_x ? sparse_infill_length(print, 10., 20., rectangle(end ? -40. : -5., end ? -30. : 5.)) :
+ sparse_infill_length(print, end ? 10. : 40., end ? 20. : 50., centered_square(5.));
+ };
+ const double end = core(adaptive, true);
+ const double middle = core(adaptive, false);
+ REQUIRE(end > 0.);
+ CHECK(middle > 0.8 * end);
+ CHECK(middle < 1.25 * end);
+ CHECK(middle < 0.7 * core(uniform, false));
+}
+
+TEST_CASE("Adaptive TPMS shells and blend keep the whole deep core of a tall object sparse", "[Fill]")
+{
+ // Distance warp grades it partly along the height, like Lobes, as its warp is centered on the middle.
+ const std::string mode = GENERATE("stepped_shells", "smooth_blend");
+ CAPTURE(mode);
+ // A 30 x 30 x 90 mm box: from 15 to 75 mm high its axis is 15 mm deep, as deep as its center.
+ auto slice = [](const std::string &adaptive, Print &print) {
+ 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"},
+ {"layer_height", 0.4},
+ {"initial_layer_print_height", 0.4}});
+ };
+ Print uniform, distance;
+ slice("disabled", uniform);
+ slice(mode, distance);
+ // At 15 to 25 mm high the axis is as sparse as at the middle, where Lobes grades it towards the bottom.
+ const Polygons core = centered_square(5.);
+ const double uniform_low = sparse_infill_length(uniform, 15., 25., core);
+ const double low = sparse_infill_length(distance, 15., 25., core);
+ const double middle = sparse_infill_length(distance, 40., 50., core);
+ CAPTURE(uniform_low, low, middle);
+ REQUIRE(middle > 0.);
+ CHECK(low < 0.7 * uniform_low);
+ CHECK(low < 1.25 * middle);
+}
+
+TEST_CASE("Adaptive TPMS infill is sparse at the center of both of two united spheres", "[Fill]")
+{
+ // Two spheres of 20 mm, their centers 32 mm apart at 20 mm high: centered on the origin, they are at x = -16 and 16.
+ TriangleMesh spheres = make_sphere(20., 2. * PI / 90.);
+ spheres.translate(-16.f, 0.f, 20.f);
+ TriangleMesh second = make_sphere(20., 2. * PI / 90.);
+ second.translate(16.f, 0.f, 20.f);
+ spheres.merge(second);
+ auto slice = [&spheres](const std::string &adaptive, Print &print) {
+ 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"},
+ {"layer_height", 0.4},
+ {"initial_layer_print_height", 0.4}});
+ };
+ Print uniform, adaptive;
+ slice("disabled", uniform);
+ slice("lobes", adaptive);
+ // Within 5 mm of either center: at most 5% + 20% * 7.1 / 20 = 12.1% dense, where one center for both
+ // would leave the other sphere at more than 60% of the uniform density.
+ for (const double x : {-16., 16.}) {
+ CAPTURE(x);
+ Polygons core = centered_square(5.);
+ for (Polygon &square : core)
+ square.translate(Point::new_scale(x, 0.));
+ const double core_uniform = sparse_infill_length(uniform, 18., 22., core);
+ REQUIRE(core_uniform > 0.);
+ CHECK(sparse_infill_length(adaptive, 18., 22., core) < 0.45 * core_uniform);
+ }
+}
+
+TEST_CASE("Adaptive TPMS gradients keep the surface density deeper in the order quadratic, linear, exponential", "[Fill]")
+{
+ // With a denser surface, t^2 <= t and the geometric interpolation is below the linear one at every depth.
+ auto length_for = [](const std::string &gradient) {
+ Print print;
+ 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},
+ {"layer_height", 0.4},
+ {"initial_layer_print_height", 0.4}});
+ return sparse_infill_length(print, 0., 40., centered_square(20.));
+ };
+ const double quadratic = length_for("quadratic");
+ const double linear = length_for("linear");
+ const double exponential = length_for("exponential");
+ CHECK(quadratic > linear);
+ CHECK(linear > exponential);
+}
+
+TEST_CASE("Adaptive TPMS settings leave the infill unchanged when they do not apply", "[Fill]")
+{
+ // Adaptive density turned off, or turned on for a pattern that is no TPMS.
+ const auto [pattern, adaptive] = GENERATE(
+ table({{"tpmsd", "disabled"}, {"tpmsfk", "disabled"}, {"gyroid", "disabled"}, {"grid", "lobes"}, {"grid", "stepped_shells"}, {"grid", "smooth_blend"}, {"grid", "distance_warp"}, {"grid", "normal_z"}}));
+ CAPTURE(pattern, adaptive);
+ Print reference, tuned;
+ Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, reference,
+ {{"sparse_infill_pattern", pattern}, {"sparse_infill_density", "20%"}, {"layer_height", 0.2}});
+ Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, tuned,
+ {{"sparse_infill_pattern", pattern},
+ {"sparse_infill_density", "20%"},
+ {"layer_height", 0.2},
+ {"tpms_adaptive", adaptive},
+ {"tpms_interior_density", "40%"},
+ {"tpms_adaptive_gradient", "exponential"}});
+ const SparseInfillShape expected = sparse_infill_shape(reference);
+ REQUIRE(expected.path_count > 0);
+ CHECK(sparse_infill_shape(tuned).sequence == expected.sequence);
+}
+
+TEST_CASE("Adaptive gyroid infill ignores the Z-buckling optimization", "[Fill]")
+{
+ auto shape_for = [](const std::string &gyroid_optimized) {
+ Print print;
+ Slic3r::Test::init_and_process_print({Slic3r::Test::cube(20)}, print,
+ {{"sparse_infill_pattern", "gyroid"},
+ {"sparse_infill_density", "10%"},
+ {"gyroid_optimized", gyroid_optimized},
+ {"tpms_adaptive", "lobes"},
+ {"layer_height", 0.2}});
+ return sparse_infill_shape(print);
+ };
+ const SparseInfillShape expected = shape_for("0");
+ REQUIRE(expected.path_count > 0);
+ CHECK(shape_for("1").sequence == expected.sequence);
+}
+
+TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger region of the same object", "[Fill]")
+{
+ const InfillPattern pattern = GENERATE(ipGyroid, ipTpmsD, ipTpmsFK);
+ CAPTURE(pattern);
+ // 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 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, [] {});
+
+ auto circle = [](double radius) {
+ Polygon contour = make_circle_num_segments(scale_(radius), 120);
+ contour.translate(Point::new_scale(100., 60.));
+ return ExPolygon(std::move(contour));
+ };
+ const ExPolygon region = circle(20.);
+ const ExPolygon larger = circle(30.);
+ auto fill = [pattern, &field](const ExPolygon &expolygon, double z) {
+ std::unique_ptr filler(Fill::new_from_type(pattern));
+ filler->spacing = 0.45;
+ filler->angle = float(M_PI / 7.);
+ filler->z = z;
+ filler->tpms_radial_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.layer_height = 0.2;
+ params.dont_adjust = true;
+ Surface surface(stInternal, expolygon);
+ return filler->fill_surface(&surface, params);
+ };
+ // Away from the boundary of the region, where both are clipped and connected the same way.
+ const Polygons inner = shrink(to_polygons(region), scale_(1.));
+ auto farthest = [&inner](const Polylines &from, const Polylines &to) {
+ const AABBTreeLines::LinesDistancer tree(to_lines(to));
+ double distance = 0.;
+ for (const Polyline &path : intersection_pl(from, inner))
+ for (const Point &point : path.equally_spaced_points(scale_(0.2)))
+ distance = std::max(distance, tree.distance_from_lines(point));
+ return unscale(distance);
+ };
+ // Marching squares simplifies rings that start elsewhere in each region. At 15.325 mm TPMS-FK has a saddle,
+ // whose lines connect one way or the other with the sampling grid.
+ const double tolerance = SPARSE_INFILL_RESOLUTION + 0.01;
+ for (const double z : {5.1, 12.3, 15.325, 20.1, 27.9, 34.7}) {
+ CAPTURE(z);
+ const Polylines paths = fill(region, z);
+ REQUIRE_FALSE(paths.empty());
+ const Polylines reference = fill(larger, z);
+ CHECK(farthest(reference, paths) < tolerance);
+ CHECK(farthest(paths, reference) < tolerance);
+ }
+}
+
+TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBridge]")
+{
+ const std::string pattern = GENERATE("tpmsd", "tpmsfk", "gyroid");
+ CAPTURE(pattern);
+ Print print;
+ 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%"},
+ {"layer_height", 0.2},
+ {"initial_layer_print_height", 0.2},
+ {"resolution", 0.012}});
+
+ const Layer &layer = *print.objects().front()->get_layer(40);
+ Polylines printed;
+ for (const LayerRegion *region : layer.regions())
+ for (const ExtrusionEntity *entity : region->fills.flatten().entities)
+ if (entity->role() == erInternalInfill)
+ entity->collect_polylines(printed);
+ REQUIRE_FALSE(printed.empty());
+ const AABBTreeLines::LinesDistancer printed_tree(to_lines(printed));
+
+ // Exclude the perimeter connections, which anchoring and extrusion trim differently.
+ const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr),
+ shrink(to_polygons(layer.lslices), scale_(3.)));
+ REQUIRE_FALSE(anchors.empty());
+ double max_distance = 0.;
+ for (const Polyline &path : anchors)
+ for (const Point &point : path.equally_spaced_points(scale_(0.25)))
+ max_distance = std::max(max_distance, printed_tree.distance_from_lines(point));
+ CHECK(unscale(max_distance) <= 0.012);
+}
diff --git a/tests/fff_print/test_gcodeprocessor.cpp b/tests/fff_print/test_gcodeprocessor.cpp
index 2d6bafd0a9..4bdc416002 100644
--- a/tests/fff_print/test_gcodeprocessor.cpp
+++ b/tests/fff_print/test_gcodeprocessor.cpp
@@ -9,6 +9,7 @@
#include "libslic3r/Config.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Model.hpp"
+#include "libslic3r/Print.hpp"
#include "libslic3r/Utils.hpp"
#include "test_helpers.hpp"
@@ -221,3 +222,62 @@ TEST_CASE("Line ends of the exported G-code mark every newline in the file", "[G
INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1);
CHECK(difference.first == result.lines_ends.end());
}
+
+TEST_CASE("Reloaded moves name their lines in G-code a script rewrote in place", "[GCodeProcessor]")
+{
+ Print print;
+ Model model;
+ Test::init_print({ Test::cube(20) }, print, model);
+ GCodeProcessorResult result;
+ const std::string gcode = Test::gcode(print, &result);
+ const auto exported_moves = result.moves;
+
+ // A script that prepends one comment and, writing in text mode on Windows, turns every LF into CRLF.
+ const std::string prepended = ";EDITED\r\n";
+ std::string edited = prepended;
+ for (const char c : gcode) {
+ if (c == '\n')
+ edited += '\r';
+ edited += c;
+ }
+ ScopedTemporaryFile temp(".gcode");
+ save_string_file(temp.path(), edited);
+ result.filename = temp.string();
+ print.reload_gcode_moves(&result);
+
+ std::vector newline_ends;
+ for (size_t i = edited.find('\n'); i != std::string::npos; i = edited.find('\n', i + 1))
+ newline_ends.push_back(i + 1);
+ CHECK(result.lines_ends == newline_ends);
+
+ // Every move that came from a line now names the same line one further down.
+ REQUIRE(result.moves.size() == exported_moves.size());
+ const auto difference = std::mismatch(exported_moves.begin(), exported_moves.end(), result.moves.begin(),
+ [](const auto &exported, const auto &reloaded) {
+ return reloaded.gcode_id == (exported.gcode_id == 0 ? 0 : exported.gcode_id + 1);
+ });
+ INFO("first difference at move " << difference.first - exported_moves.begin());
+ CHECK(difference.first == exported_moves.end());
+}
+
+TEST_CASE("Rewritten G-code that cannot be re-read keeps the moves and hides the G-code window", "[GCodeProcessor]")
+{
+ Print print;
+ Model model;
+ Test::init_print({ Test::cube(20) }, print, model);
+ GCodeProcessorResult result;
+ const std::string gcode = Test::gcode(print, &result);
+ const auto exported_moves = result.moves;
+
+ // A script that strips the trailing config block, which the G-code reader needs.
+ const size_t config_block = gcode.find("; CONFIG_BLOCK_START");
+ REQUIRE(config_block != std::string::npos);
+ ScopedTemporaryFile temp(".gcode");
+ save_string_file(temp.path(), gcode.substr(0, config_block));
+ result.filename = temp.string();
+ print.reload_gcode_moves(&result);
+
+ CHECK(result.lines_ends.empty());
+ REQUIRE(result.moves.size() == exported_moves.size());
+ CHECK(result.moves.back().gcode_id == exported_moves.back().gcode_id);
+}
diff --git a/tests/libslic3r/CMakeLists.txt b/tests/libslic3r/CMakeLists.txt
index cf9a69e5d6..21aa27bf29 100644
--- a/tests/libslic3r/CMakeLists.txt
+++ b/tests/libslic3r/CMakeLists.txt
@@ -3,6 +3,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp
test_3mf.cpp
+ test_amf.cpp
# Round-trip seam metadata and active/dormant volume settings in both formats.
test_precise_seam_3mf.cpp
# Pure perimeter extraction is independent of Print/Layer fixtures.
@@ -34,6 +35,7 @@ add_executable(${_TEST_NAME}_tests
test_fill_corner_smoothing.cpp
test_filament_mixer.cpp
test_fill_plane_path.cpp
+ test_fill_tpms_adaptive.cpp
test_geometry.cpp
test_multimaterial_segmentation.cpp
test_placeholder_parser.cpp
diff --git a/tests/libslic3r/test_3mf.cpp b/tests/libslic3r/test_3mf.cpp
index 9570cb3ecf..8439e8f846 100644
--- a/tests/libslic3r/test_3mf.cpp
+++ b/tests/libslic3r/test_3mf.cpp
@@ -45,6 +45,7 @@
#include
#include // for std::enable_if_t
#include // for typeid
+#include
#include
#include
@@ -415,6 +416,68 @@ TEST_CASE("A project with a plate id below 1 fails to load", "[3mf][Regression]"
REQUIRE_FALSE(loaded);
}
+TEST_CASE("A project whose components reference themselves fails to load", "[3mf][Regression]")
+{
+ // One self-reference keeps the expansion going without ever reaching a mesh. A thousand also make
+ // each expansion queue a thousand more, so the bound has to hold the work list, not just the loop.
+ const int references = GENERATE(1, 1000);
+ INFO("self-references " << references);
+
+ ScopedTemporaryFile temp(".3mf");
+ store_painted_cube(temp.string());
+
+ // Point the component back at the object that holds it, repeated `references` times.
+ REQUIRE(rewrite_3mf_entries(temp.string(), [references](std::string& name, std::string& data) {
+ if (!boost::algorithm::ends_with(name, "3dmodel.model"))
+ return false;
+ std::smatch match;
+ if (!std::regex_search(data, match, std::regex("