Compare commits

...
Author SHA1 Message Date
Hanif Koh 3071d525ee Expose App, GL, Printer and Project Facts to Plugins
Plugins that help users file bug reports could only guess these from log lines: the app config is
deny-listed, and the project path, a project export and the device list had no API.

orca.host gains app_info(), gl_info() and selected_printer(), and Plater gains project_path() and
export_3mf_copy(path). The copy export leaves the project's file name, saved state and model
unchanged, does not write the signed-in account as the designer, and raises the audit event of
open(path, "w") so the plugin gets the same permissions as for writing the file itself.
2026-10-10 03:35:39 +08:00
Misterff1 b5ef24e7ff Fix regression: Arc Fitting setting for BBL P2S (#16319)
Disable Arc Fitting for BBL P2S
2026-10-10 00:32:46 +08:00
Ian Chua 8585eae816 fix: hide symbols of the bundled static openssl (#16317)
* fix: hide symbols of the bundled static openssl

* fix: hide the bundled static OpenSSL symbols on Linux

* fix: relink _ssl/_hashlib when OpenSSL recipe changes
2026-10-10 00:26:02 +08:00
Lam Wei Lun e72ace164b feat(speed-dial): add plugin page capabilities as actions
Plugin Pages capabilities (top-level notebook tabs) were missing from the Speed Dial because ActionRegistry only ingested Script capabilities.

Enumerate and subscribe to Pages as well. Launching a page action switches the notebook to that page, swapping it into the visible slot first when it lives behind the overflow dropdown.
2026-10-09 23:44:52 +08:00
Ian BassiandRodrigo Faselli b5f5b50157 Adaptive TPMS (#16005)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-09 12:36:09 -03:00
e0b35f9ec9 Fix garbled G-code preview when a post-processing script is used (#15005)
* Rebuild the G-code line offsets after post-processing scripts run in place

* Clamp the G-code window reads to the mapped file size

* Add tests for rebuilding the G-code line offsets

* Include <mutex>, <ios> and boost/filesystem/operations.hpp where they are used

* Keep the preview's G-code lines and highlight in step with post-processing scripts

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-09 23:08:43 +08:00
Ian Chua eb28daf0fe fix: persist user id so that setting_id doesn't get wiped (#16246) 2026-10-09 21:00:27 +08:00
ExPikaPaka 6ac7ae24a3 Block undo and redo while a background job runs (#16015)
* Block undo and redo while a background job runs

A job is queued against the model as it stands and hands its result back
when it finishes, so undoing underneath it leaves that result landing on
geometry it was never computed for. Undo and redo now wait for the job
and say so, and can_undo()/can_redo() report the same, so the toolbar
and the menu items stay in step.

* Say what to do about the running operation, not just that it blocks

Review feedback: "Stop it first" is the only way out the old text offered,
and stopping is rarely what the user wants. Waiting for the operation to
finish works just as well, so the notification now names both.
2026-10-09 19:26:37 +08:00
ExPikaPaka d55e32fed4 Stop reading a missing AMF metadata type as a string (#16060)
A `<metadata>` element without a `type` attribute makes
`get_attribute()` return nullptr, which was then assigned to a
`std::string` and read as a C string.

Check it the way the sibling metadata handler already does, and stop the
parse.

Regression test in `tests/libslic3r/test_amf.cpp`, a new file: the suite
had no AMF test at all.
2026-10-09 19:25:53 +08:00
SoftFever cd0b529e31 Reject 3MF component references that form a cycle (#16059) 2026-10-09 19:23:20 +08:00
59 changed files with 2757 additions and 94 deletions
+29
View File
@@ -44,6 +44,18 @@ else()
if(APPLE) if(APPLE)
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET}) set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
else() 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") set(_conf_cmd env "CC=${CMAKE_C_COMPILER}" "LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}" "./config")
endif() endif()
set(_cross_comp_prefix_line "") 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" COMMAND ${CMAKE_COMMAND} -E copy_directory openssl "${DESTDIR}${CMAKE_INSTALL_LIBDIR}/cmake/openssl"
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}" 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 ()
+24
View File
@@ -299,3 +299,27 @@ endif()
if(TARGET dep_ZLIB) if(TARGET dep_ZLIB)
add_dependencies(dep_python3 dep_ZLIB) add_dependencies(dep_python3 dep_ZLIB)
endif() 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 ()
+221
View File
@@ -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.
@@ -10,6 +10,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"4000", "4000",
"4000" "4000"
@@ -10,6 +10,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"4000", "4000",
"4000", "4000",
@@ -9,6 +9,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"4000", "4000",
"4000" "4000"
@@ -9,6 +9,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"4000", "4000",
"4000" "4000"
@@ -10,6 +10,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"4000", "4000",
"4000", "4000",
@@ -11,6 +11,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"4000", "4000",
"4000", "4000",
@@ -11,6 +11,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000", "10000",
@@ -11,6 +11,7 @@
"30", "30",
"30" "30"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000", "10000",
@@ -11,6 +11,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"4000", "4000",
"4000", "4000",
@@ -11,6 +11,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000", "10000",
@@ -11,6 +11,7 @@
"30", "30",
"30" "30"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000", "10000",
@@ -10,6 +10,7 @@
"30", "30",
"30" "30"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000" "10000"
@@ -10,6 +10,7 @@
"50", "50",
"50" "50"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000", "10000",
@@ -11,6 +11,7 @@
"30", "30",
"30" "30"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000", "10000",
@@ -10,6 +10,7 @@
"30", "30",
"30" "30"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000" "10000"
@@ -10,6 +10,7 @@
"30", "30",
"30" "30"
], ],
"enable_arc_fitting": "0",
"default_acceleration": [ "default_acceleration": [
"10000", "10000",
"10000" "10000"
+2
View File
@@ -180,6 +180,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp Fill/FillRectilinear.hpp
Fill/FillTpmsAdaptive.cpp
Fill/FillTpmsAdaptive.hpp
Fill/FillTpmsD.cpp Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp Fill/FillTpmsFK.cpp
+33 -6
View File
@@ -311,6 +311,11 @@ struct SurfaceFillParams
// For Gyroid: when true, use the parameterized "optimized" wave. // For Gyroid: when true, use the parameterized "optimized" wave.
bool gyroid_optimized = false; 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]. // Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. }; double smooth_factor { 0. };
@@ -353,6 +358,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth); RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle); RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized); 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(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern); RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills); RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -386,6 +394,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern && this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills && this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized && 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->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order; this->fill_order == rhs.fill_order;
} }
@@ -994,15 +1005,23 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
// Orca: apply fill multiline only for sparse infill // Orca: apply fill multiline only for sparse infill
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1; params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
// Pass through gyroid_optimized only when the effective pattern is Gyroid, // Orca: Pass through separated_infills only where it can move the pattern.
// 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.
params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) && params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) &&
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface; 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) { if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value, params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
region_config.sparse_infill_rotate_template.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->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle; f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; 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_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config(); f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) { 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.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle; params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized; 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; params.smooth_factor = surface_fill.params.smooth_factor;
// BBS // BBS
@@ -1575,6 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->angle = surface_fill.params.angle; f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle; f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr; 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_config = &this->object()->print()->config();
f->print_object_config = &this->object()->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.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline; params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized; 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; params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns. // Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds // Without the sparse extrusion role, the filler uses each surface's bounds
+9
View File
@@ -33,6 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r { namespace Slic3r {
class Surface; class Surface;
class TpmsRadialField;
enum InfillPattern : int; enum InfillPattern : int;
namespace FillAdaptive { namespace FillAdaptive {
@@ -91,6 +92,11 @@ struct FillParams
// For Gyroid: when true, use the parameterized "optimized" variant. // For Gyroid: when true, use the parameterized "optimized" variant.
bool gyroid_optimized { false }; 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]. // Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. }; double smooth_factor { 0. };
@@ -155,6 +161,9 @@ public:
// Octree builds on mesh for usage in the adaptive cubic infill // Octree builds on mesh for usage in the adaptive cubic infill
FillAdaptive::Octree* adapt_fill_octree = nullptr; 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). // PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function. // Orca: also used by gap fill function.
const PrintConfig *print_config = nullptr; const PrintConfig *print_config = nullptr;
+26 -5
View File
@@ -17,6 +17,17 @@
#include "libslic3r/Polyline.hpp" #include "libslic3r/Polyline.hpp"
#include "FillGyroid.hpp" #include "FillGyroid.hpp"
#include "libslic3r/Polygon.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. // 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 float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const
{ {
const float a = fx * float(x); return gyroid(fx * float(x), fy * float(y), fz * float(z_arg));
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);
} }
float get_scalar(Coord p) const float get_scalar(Coord p) const
@@ -307,6 +315,14 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon, ExPolygon expolygon,
Polylines &polylines_out) 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.); auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
if(std::abs(infill_angle) >= EPSILON) if(std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle); expolygon.rotate(-infill_angle);
@@ -326,7 +342,12 @@ void FillGyroid::_fill_surface_single(
// generate pattern // generate pattern
Polylines polylines; 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 = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches // Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj. // the standard parametric path's wavelength: 2*pi * spacing / density_adj.
// omega >= 1 always, so fz >= baseline -> shorter vertical wavelength -> // omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
+823
View File
@@ -0,0 +1,823 @@
#include "FillTpmsAdaptive.hpp"
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <deque>
#include <functional>
#include <limits>
#include <utility>
#include <vector>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#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<float>::infinity();
constexpr double InfD = std::numeric_limits<double>::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<float> &grid, const Vec3i32 &size, int axis, const std::function<void()> &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<size_t>(0, size_t(size[a1]) * size[a2]), [&](const tbb::blocked_range<size_t> &range) {
std::vector<float> f(n), d(n);
std::vector<int> v(n);
std::vector<double> 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<std::vector<double>> 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<double> &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<double, 257> m_scale;
};
} // namespace
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled)
: m_mode(mode)
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
{
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<float> depth(sz * m_size.z(), 0.f);
tbb::parallel_for(tbb::blocked_range<int>(0, m_size.z()), [&](const tbb::blocked_range<int> &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<std::ptrdiff_t, 6> 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<std::ptrdiff_t> 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<size_t> 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<size_t> 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<std::vector<size_t>> ties;
while (!peaks.empty()) {
const size_t front = peaks.front();
std::vector<char> joins(peaks.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(1, peaks.size()), [&](const tbb::blocked_range<size_t> &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<size_t> &tied = ties.emplace_back(1, front);
std::vector<size_t> 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<size_t> &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<int> 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<double> &values, size_t count) {
for (int pass = 0; pass < 2; ++pass) {
std::vector<double> 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<size_t>(0, m_lobes.size()), [&](const tbb::blocked_range<size_t> &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<std::pair<double, size_t>> 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<double> 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<double> mean(log_reach.size() * (Samples + 1));
for (size_t d = 0; d < log_reach.size(); ++d) {
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
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<bool> 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<size_t> 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<bool, 6> 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<int, 3> n0;
std::array<double, 3> 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 &center) 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>(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<std::pair<double, size_t>, 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<double, MaxMorph> 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<AdaptiveTpmsField>
{
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<TpmsLevelField>
{
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<marchsq::Ring> 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<TpmsShell> 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<TpmsShell> 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 &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
{
FillParams shell_params = params;
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
shell_params.density = shell.density;
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
fill_shell(shell_params, part);
}
}
} // namespace Slic3r
+143
View File
@@ -0,0 +1,143 @@
#pragma once
#include <array>
#include <cstddef>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
#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<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled);
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
static constexpr size_t MaxMorph = 4;
using Radials = std::array<Radial, 2 * MaxMorph>;
// 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<float> reach;
// Distance warp: mean depth over the ball along each direction, sampled up to the reach.
std::vector<float> 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 &center) 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<int> m_body;
std::vector<Body> m_bodies;
std::vector<Lobe> m_lobes;
// In the 2D modes, the nearest section with a body to every section.
std::vector<int> m_section;
// In the modes following the distance to the surface, the depth of every grid node.
std::vector<float> m_depth;
};
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
// A field for every adaptive mode in use, indexed by the mode.
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
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<TpmsShell> 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 &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r
+43 -18
View File
@@ -14,8 +14,10 @@
#include "libslic3r/Fill/FillBase.hpp" #include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp" #include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp" #include "FillTpmsD.hpp"
#include "FillTpmsAdaptive.hpp"
namespace Slic3r { namespace Slic3r {
@@ -23,6 +25,11 @@ static double scaled_floor(double x,double scale){
return std::floor(x/scale)*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) static Polylines make_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
{ {
const double scaleFactor = scale_(line_spacing) / density_adjusted; const double scaleFactor = scale_(line_spacing) / density_adjusted;
@@ -110,31 +117,49 @@ void FillTpmsD::_fill_surface_single(
ExPolygon expolygon, ExPolygon expolygon,
Polylines &polylines_out) 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.); auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
if(std::abs(infill_angle) >= EPSILON) if(std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle); expolygon.rotate(-infill_angle);
BoundingBox bb = expolygon.contour.bounding_box(); Polylines polylines;
// Density adjusted to have a good %of weight. if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline); // Radians per mm of the regular pattern at a density.
// Distance between the gyroid waves in scaled coordinates. auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
coord_t distance = coord_t(scale_(this->spacing) / density_adjusted); 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 // 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))); bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
// generate pattern // generate pattern
Polylines polylines = make_waves( polylines = make_waves(
scale_(this->z), scale_(this->z),
density_adjusted, density_adjusted,
this->spacing, this->spacing,
ceil(bb.size()(0) / distance) + 1., ceil(bb.size()(0) / distance) + 1.,
ceil(bb.size()(1) / 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 // Apply multiline offset if needed
multiline_fill(polylines, params, spacing); multiline_fill(polylines, params, spacing);
+33 -15
View File
@@ -8,6 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp" #include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp" #include "FillTpmsFK.hpp"
#include "FillTpmsAdaptive.hpp"
#include <cmath> #include <cmath>
#include <algorithm> #include <algorithm>
#include <cstddef> #include <cstddef>
@@ -17,6 +18,18 @@
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
#include "libslic3r/Polygon.hpp" #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 { namespace marchsq {
using namespace Slic3r; using namespace Slic3r;
@@ -42,16 +55,7 @@ struct ScalarField
{} {}
// Get the scalar field value at x,y,z in coordf_t coordinates. // 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 float get_scalar(coordf_t x, coordf_t y, coordf_t z) const { return fischer_koch(freq * x, freq * y, freq * z); }
{
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);
}
// Get the scalar field value at a Coord for the current z value. // Get the scalar field value at a Coord for the current z value.
float get_scalar(Coord p) const float get_scalar(Coord p) const
@@ -128,20 +132,34 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
ExPolygon expolygon, ExPolygon expolygon,
Polylines& polylines_out) 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.); auto infill_angle = float(this->angle + (CorrectionAngle * 2 * M_PI) / 360.);
if (std::abs(infill_angle) >= EPSILON) if (std::abs(infill_angle) >= EPSILON)
expolygon.rotate(-infill_angle); expolygon.rotate(-infill_angle);
float density_factor = std::min(0.9f, params.density);
// Density (field period) adjusted to have a good %of weight. // Density (field period) adjusted to have a good %of weight.
const float vari_T = 4.18f * spacing * params.multiline / density_factor; auto period = [&params, this](float density) { return 4.18f * spacing * params.multiline / std::min(0.9f, density); };
BoundingBox bbox = expolygon.contour.bounding_box(); BoundingBox bbox = expolygon.contour.bounding_box();
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges. // Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing)); bbox.offset(scale_((params.multiline + 1) * spacing));
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, vari_T); Polylines polylines;
// Get simplified lines using coarse tolerance of 0.1mm (this is infill). if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
Polylines polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION); 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 // Apply multiline offset if needed
multiline_fill(polylines, params, spacing); multiline_fill(polylines, params, spacing);
+7 -2
View File
@@ -314,8 +314,13 @@ void AMFParserContext::startElement(const char *name, const char **atts)
case 2: case 2:
if (strcmp(name, "metadata") == 0) { if (strcmp(name, "metadata") == 0) {
if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) { if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) {
m_value[0] = get_attribute(atts, "type"); const char *type = get_attribute(atts, "type");
node_type_new = NODE_TYPE_METADATA; 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) }/* else if (strcmp(name, "layer_config_ranges") == 0 && m_path[1] == NODE_TYPE_OBJECT)
node_type_new = NODE_TYPE_LAYER_CONFIG;*/ node_type_new = NODE_TYPE_LAYER_CONFIG;*/
+6 -2
View File
@@ -437,10 +437,14 @@ coordf_t Layer::get_sparse_infill_max_void_area()
double max_void_area = 0.; double max_void_area = 0.;
for (auto layerm : m_regions) { for (auto layerm : m_regions) {
Flow flow = layerm->flow(frInfill); Flow flow = layerm->flow(frInfill);
float density = layerm->region().config().sparse_infill_density; const PrintRegionConfig &config = layerm->region().config();
InfillPattern pattern = layerm->region().config().sparse_infill_pattern; float density = config.sparse_infill_density;
InfillPattern pattern = config.sparse_infill_pattern;
if (density == 0.) if (density == 0.)
return -1; 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 //BBS: rough estimation and need to be optimized
double spacing = flow.scaled_spacing() * (100 - density) / density; double spacing = flow.scaled_spacing() * (100 - density) / density;
+11 -2
View File
@@ -1181,6 +1181,9 @@ static std::vector<std::string> s_Preset_print_options{
"is_infill_first", "is_infill_first",
"sparse_infill_density", "sparse_infill_density",
"fill_multiline", "fill_multiline",
"tpms_adaptive",
"tpms_interior_density",
"tpms_adaptive_gradient",
"gyroid_optimized", "gyroid_optimized",
"sparse_infill_pattern", "sparse_infill_pattern",
"sparse_infill_smooth_factor", "sparse_infill_smooth_factor",
@@ -2345,7 +2348,7 @@ bool PresetCollection::reset_project_embedded_presets()
return re_select; 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(); lock();
const std::string canonical_name = this->canonical_preset_name(name); 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(); 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) if (update_time > 0)
preset->updated_time = update_time; preset->updated_time = update_time;
if (preset->sync_info == "update") if (preset->sync_info == "update")
@@ -2653,6 +2659,9 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
iter->base_id = based_id; iter->base_id = based_id;
iter->filament_id = cloud_filament_id; iter->filament_id = cloud_filament_id;
update_alias(*iter); 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); //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%") 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; % iter->name %Preset::get_type_string(m_type) %iter->setting_id %iter->base_id %iter->sync_info %iter->inherits() % iter->filament_id;
+1 -1
View File
@@ -603,7 +603,7 @@ public:
void update_after_user_presets_loaded(); void update_after_user_presets_loaded();
//BBS: get user presets //BBS: get user presets
int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &result_presets); int get_user_presets(PresetBundle *preset_bundle, std::vector<Preset> &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); bool need_sync(std::string name, std::string setting_id, long long update_time);
//BBS: add function to generate differed preset for save //BBS: add function to generate differed preset for save
+25
View File
@@ -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<std::mutex> lock(result->result_mutex);
result->lines_ends.clear();
return;
}
GCodeProcessorResult& reloaded = processor.result();
std::lock_guard<std::mutex> lock(result->result_mutex);
result->moves = std::move(reloaded.moves);
result->lines_ends = std::move(reloaded.lines_ends);
}
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
{ {
if (config().skirt_loops == 0 || config().skirt_type != stPerObject || m_objects.empty()) if (config().skirt_loops == 0 || config().skirt_type != stPerObject || m_objects.empty())
+9
View File
@@ -12,6 +12,7 @@
#include "PrintBase.hpp" #include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp" #include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp" #include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp"
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp" #include "ExtrusionEntityCollection.hpp"
@@ -406,6 +407,7 @@ public:
double max_z() const { return m_max_z; } double max_z() const { return m_max_z; }
// Centering offset of the sliced mesh from the scaled and rotated mesh of the model. // 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 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 // BBS
void generate_support_preview(); void generate_support_preview();
@@ -650,6 +652,7 @@ private:
FillAdaptive::RegionOctrees prepare_adaptive_infill_data( FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const; const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data(); FillLightning::GeneratorPtr prepare_lightning_infill_data();
TpmsRadialFields prepare_tpms_radial_fields() const;
// BBS // BBS
SupportNecessaryType is_support_necessary(); SupportNecessaryType is_support_necessary();
@@ -700,6 +703,7 @@ private:
FillAdaptive::RegionOctrees m_adaptive_fill_octrees; FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes; std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator; FillLightning::GeneratorPtr m_lightning_generator;
TpmsRadialFields m_tpms_radial_fields;
std::vector < VolumeSlices > firstLayerObjSliceByVolume; std::vector < VolumeSlices > firstLayerObjSliceByVolume;
std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups; std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups;
@@ -1287,6 +1291,11 @@ public:
void set_gcode_file_ready(); void set_gcode_file_ready();
void set_gcode_file_invalidated(); void set_gcode_file_invalidated();
void export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr); 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 //BBS: add modify_count logic
int get_modified_count() const {return m_modified_count;} int get_modified_count() const {return m_modified_count;}
//BBS: add status for whether support used //BBS: add status for whether support used
+87
View File
@@ -364,6 +364,26 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{
}; };
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(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 //BBS
static t_config_enum_values s_keys_map_PrintSequence { static t_config_enum_values s_keys_map_PrintSequence {
{ "by layer", int(PrintSequence::ByLayer) }, { "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->max = 10; // Maximum number of lines for infill pattern
def->set_default_value(new ConfigOptionInt(1)); 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<TpmsAdaptiveMode>::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>(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<TpmsAdaptiveGradient>::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>(TpmsAdaptiveGradient::Linear));
// Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z // 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 // (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 // Z-axis compression. Filament use at the same `sparse_infill_density` setting is
+27
View File
@@ -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. // 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; } 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. // 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 // 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, 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 //BBS
enum class PrintSequence { enum class PrintSequence {
ByLayer, 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(ToolChangeOrderingType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode) CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder) 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 #undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -1450,6 +1474,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca: // Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height)) ((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline)) ((ConfigOptionInt, fill_multiline))
((ConfigOptionEnum<TpmsAdaptiveMode>, tpms_adaptive))
((ConfigOptionPercent, tpms_interior_density))
((ConfigOptionEnum<TpmsAdaptiveGradient>, tpms_adaptive_gradient))
((ConfigOptionBool, gyroid_optimized)) ((ConfigOptionBool, gyroid_optimized))
// Ironing options // Ironing options
((ConfigOptionEnum<IroningType>, ironing_type)) ((ConfigOptionEnum<IroningType>, ironing_type))
+37
View File
@@ -31,6 +31,7 @@
#include "Fill/FillAdaptive.hpp" #include "Fill/FillAdaptive.hpp"
#include "Fill/Fill.hpp" #include "Fill/Fill.hpp"
#include "Fill/FillLightning.hpp" #include "Fill/FillLightning.hpp"
#include "Fill/FillTpmsAdaptive.hpp"
#include "format.hpp" #include "format.hpp"
#include "AABBTreeIndirect.hpp" #include "AABBTreeIndirect.hpp"
#include "AABBTreeLines.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(); 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<bool, size_t(TpmsAdaptiveMode::Count)> modes{};
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id)
if (const PrintRegionConfig &config = this->printing_region(region_id).config();
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_tpms_adaptive_pattern(config.sparse_infill_pattern))
modes[size_t(config.tpms_adaptive.value)] = true;
modes[size_t(TpmsAdaptiveMode::Disabled)] = false;
if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty())
return fields;
std::vector<TpmsRadialField::Slice> 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<TpmsRadialField>(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); });
if (!field->empty())
fields[mode] = std::move(field);
}
return fields;
}
void PrintObject::clear_layers() void PrintObject::clear_layers()
{ {
if (!m_shared_object) { if (!m_shared_object) {
@@ -1700,6 +1733,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_overhang_angle") { || opt_key == "infill_overhang_angle") {
steps.emplace_back(posInfill); steps.emplace_back(posInfill);
} else if (opt_key == "sparse_infill_pattern" } 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. // Orca: Body centering now also determines bridge anchors during preparation.
// Invalidating preparation also invalidates infill, including top/bottom surfaces. // Invalidating preparation also invalidates infill, including top/bottom surfaces.
|| opt_key == "center_of_surface_pattern" || 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_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
this->m_tpms_radial_fields = this->prepare_tpms_radial_fields();
std::vector<size_t> layers_to_generate_infill; std::vector<size_t> layers_to_generate_infill;
for (const auto &pair : surfaces_by_layer) { for (const auto &pair : surfaces_by_layer) {
+95 -5
View File
@@ -149,6 +149,56 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name); return std::make_unique<PluginScriptAction>(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/<icon>.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<AppAction> 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<PluginPageAction>(plugin_key, capability, source_name);
}
// ---- built-in command actions (the speed dial "commands" section) ------ // ---- built-in command actions (the speed dial "commands" section) ------
constexpr const char* kSettingPrefix = "orca_setting"; constexpr const char* kSettingPrefix = "orca_setting";
@@ -311,13 +361,20 @@ void ActionRegistry::init()
}); });
}; };
auto on_capability = [this](const PluginCapabilityId& capability, ActionChange change) { 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; return;
const std::string plugin_key = capability.plugin_key; if (!wxTheApp || wxGetApp().is_closing())
const std::string name = capability.name; return;
wxGetApp().CallAfter([this, plugin_key, name, change] { const PluginCapabilityType type = capability.type;
if (!wxGetApp().is_closing()) 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); 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)); 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). // 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 // 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 // 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)) if (auto action = make_action(plugin_key, capability->name(), source_name))
upsert(std::move(action)); 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) 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); 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<AppAction> action) void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
{ {
assert(wxThread::IsMain()); assert(wxThread::IsMain());
+1
View File
@@ -239,6 +239,7 @@ private:
// one plugin's whole action set; refresh_capability touches a single capability. // one plugin's whole action set; refresh_capability touches a single capability.
void refresh_source(const std::string& plugin_key, ActionChange change); 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_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 bool m_started = false; // init() runs exactly once; guards double-subscription
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
+5 -4
View File
@@ -283,11 +283,12 @@ void BackgroundSlicingProcess::process_fff()
m_temp_output_path = this->get_current_plate()->get_tmp_gcode_path(); m_temp_output_path = this->get_current_plate()->get_tmp_gcode_path();
m_fff_print->export_gcode(m_temp_output_path, m_gcode_result, m_fff_print->export_gcode(m_temp_output_path, m_gcode_result,
[this](const ThumbnailsParams& params) { return this->render_thumbnails(params); }); [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 // Orca: BBL printers post-process the g-code in place here, in the file the G-code viewer maps, so
// GCodeProcessorResult, so m_gcode_result->nozzle_group_result (consumed by the H2C print-dispatch // the preview re-reads its moves and line offsets from the edited file. The rest of m_gcode_result,
// nozzle mapping) survives post-processing. No preservation guard is needed on this path. // including nozzle_group_result (consumed by the H2C print-dispatch nozzle mapping), is kept.
if (m_fff_print->is_BBL_printer()) { 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"); BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": export gcode finished");
+10 -2
View File
@@ -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 || 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; 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<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
is_tpms_adaptive_pattern(pattern);
toggle_line("tpms_adaptive", have_tpms_infill);
bool have_tpms_adaptive = have_tpms_infill && config->opt_enum<TpmsAdaptiveMode>("tpms_adaptive") != TpmsAdaptiveMode::Disabled;
toggle_line("tpms_interior_density", have_tpms_adaptive);
toggle_line("tpms_adaptive_gradient", have_tpms_adaptive);
// gyroid_optimized only applies when the sparse infill pattern is gyroid without adaptive density;
// hide the whole line otherwise. // 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 there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
if (have_infill) { if (have_infill) {
+8 -2
View File
@@ -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) { auto update_lines = [this](uint64_t start_id, uint64_t end_id) {
std::vector<Line> ret; std::vector<Line> ret;
ret.reserve(end_id - start_id + 1); 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) { for (uint64_t id = start_id; id <= end_id; ++id) {
// read line from file // read line from file
const size_t start = id == 1 ? 0 : m_lines_ends[id - 2]; // Keep one entry per id: render() indexes m_lines by (id - start_id).
const size_t original_len = m_lines_ends[id - 1] - start; 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. // 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); const size_t len = std::min(original_len, (size_t) 55 * 4);
std::string gline(m_file.data() + start, len); std::string gline(m_file.data() + start, len);
+4 -4
View File
@@ -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; BOOST_LOG_TRIVIAL(trace) << "sync_preset: sync operation: " << preset->sync_info << " success! preset = " << preset->name;
if (preset->type == Preset::Type::TYPE_FILAMENT) { 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) { } 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) { } 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) ? OrcaCloudServiceAgent::generate_uuid_for_setting_id(preset.name, user_id)
: preset.setting_id; : preset.setting_id;
if (preset_id == 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; break;
} }
} }
+2
View File
@@ -396,6 +396,8 @@ public:
Slic3r::TaskManager* getTaskManager() { return m_task_manager; } Slic3r::TaskManager* getTaskManager() { return m_task_manager; }
HMSQuery* get_hms_query() { return hms_query; } HMSQuery* get_hms_query() { return hms_query; }
NetworkAgent* getAgent() { return m_agent; } NetworkAgent* getAgent() { return m_agent; }
// Version that wrote the app config before this run; empty when there was no app config.
const boost::optional<Semver>& last_config_version() const { return m_last_config_version; }
// Reconcile the live printer agent with the stored preset selection. // Reconcile the live printer agent with the stored preset selection.
void switch_printer_agent(); void switch_printer_agent();
+3
View File
@@ -148,6 +148,9 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
{"sparse_infill_density", "", 1}, {"sparse_infill_density", "", 1},
{"fill_multiline", "", 1}, {"fill_multiline", "", 1},
{"sparse_infill_pattern", "", 1}, {"sparse_infill_pattern", "", 1},
{"tpms_adaptive", "", 1},
{"tpms_interior_density", "", 1},
{"tpms_adaptive_gradient", "", 1},
{"sparse_infill_smooth_factor", "", 1}, {"sparse_infill_smooth_factor", "", 1},
{"lateral_lattice_angle_1", "", 1}, {"lateral_lattice_angle_1", "", 1},
{"lateral_lattice_angle_2", "", 1}, {"lateral_lattice_angle_2", "", 1},
+3
View File
@@ -47,6 +47,9 @@ public:
const std::string& get_vendor() const; const std::string& get_vendor() const;
const std::string& get_renderer() const; const std::string& get_renderer() const;
// False until a GL context has been made current and queried; the getters below detect
// on first use, which needs a current context.
bool is_detected() const { return m_detected; }
bool is_core_profile() const { return m_core_profile; } bool is_core_profile() const { return m_core_profile; }
bool is_mesa() const; bool is_mesa() const;
+53 -12
View File
@@ -15076,7 +15076,8 @@ bool Plater::priv::undo_redo_blocked_by_job()
return false; return false;
notification_manager->push_notification(NotificationType::CustomNotification, notification_manager->push_notification(NotificationType::CustomNotification,
NotificationManager::NotificationLevel::RegularNotificationLevel, 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; return true;
} }
@@ -19317,6 +19318,38 @@ void Plater::send_gcode_finish(wxString name)
auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name); auto out_str = GUI::format(_L("The file %s has been sent to the printer's storage space and can be viewed on the printer."), name);
p->notification_manager->push_exporting_finished_notification(out_str, "", false); p->notification_manager->push_exporting_finished_notification(out_str, "", false);
} }
namespace {
// export_3mf() assigns archive paths to previously unsaved SVGs. Puts them back, so an export that
// is not a project save does not change what a later project save writes.
class SvgArchivePathsRestorer
{
public:
explicit SvgArchivePathsRestorer(Model& model)
{
for (ModelObject* object : model.objects)
for (ModelVolume* volume : object->volumes)
if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) {
std::string* path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf;
m_paths.emplace_back(path_in_3mf, *path_in_3mf);
}
}
~SvgArchivePathsRestorer() { restore(); }
SvgArchivePathsRestorer(const SvgArchivePathsRestorer&) = delete;
SvgArchivePathsRestorer& operator=(const SvgArchivePathsRestorer&) = delete;
void restore()
{
for (const auto& [path_in_3mf, previous_path] : m_paths)
*path_in_3mf = previous_path;
}
private:
std::vector<std::pair<std::string*, std::string>> m_paths;
};
} // namespace
void Plater::export_core_3mf() void Plater::export_core_3mf()
{ {
wxString path = p->get_export_file(FT_3MF); wxString path = p->get_export_file(FT_3MF);
@@ -19325,6 +19358,23 @@ void Plater::export_core_3mf()
export_3mf(path_u8, SaveStrategy::Silence); export_3mf(path_u8, SaveStrategy::Silence);
} }
bool Plater::export_3mf_copy(const boost::filesystem::path& output_path)
{
Model& model = p->model;
SvgArchivePathsRestorer svg_paths(model);
// With no design info, export_3mf() writes the signed-in account's user id as the designer; an
// empty one keeps it out. export_3mf() also drops design info without a designer name afterwards.
const std::shared_ptr<ModelDesignInfo> design_info = model.design_info;
if (design_info == nullptr)
model.design_info = std::make_shared<ModelDesignInfo>();
ScopeGuard restore_design_info([&model, design_info]() { model.design_info = design_info; });
// The project save's layout, plus Silence so the project file name stays as it is. Unlike a save
// it never adds FullPathSources: a copy is for sharing, and those are paths on this machine.
return export_3mf(output_path, SaveStrategy::SplitModel | SaveStrategy::ShareMesh | SaveStrategy::Silence) == 0;
}
// Export the current project as a "published" 3MF: a pure export that never touches the // Export the current project as a "published" 3MF: a pure export that never touches the
// project's file name, dirty state, backup path or title, and attaches the published metadata // project's file name, dirty state, backup path or title, and attaches the published metadata
// to the model only for the duration of the export (a later Save Project is a normal 3MF). // to the model only for the duration of the export (a later Save Project is a normal 3MF).
@@ -19374,19 +19424,10 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
std::string(); std::string();
const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string(); const std::string prev_payload = had_payload ? model.model_info->metadata_items.at(ORCA_PUBLISHED_CONFIG_TAG) : std::string();
// export_3mf() assigns archive paths to previously unsaved SVGs. Preserve those fields too, SvgArchivePathsRestorer svg_paths(model);
// otherwise a publish changes what a later normal project save writes.
std::vector<std::pair<std::string*, std::string>> previous_svg_paths;
for (ModelObject* object : model.objects)
for (ModelVolume* volume : object->volumes)
if (volume != nullptr && volume->emboss_shape.has_value() && volume->emboss_shape->svg_file.has_value()) {
std::string* path_in_3mf = &volume->emboss_shape->svg_file->path_in_3mf;
previous_svg_paths.emplace_back(path_in_3mf, *path_in_3mf);
}
auto restore_temporary_state = [&]() { auto restore_temporary_state = [&]() {
for (const auto& [path_in_3mf, previous_path] : previous_svg_paths) svg_paths.restore();
*path_in_3mf = previous_path;
if (!had_model_info) { if (!had_model_info) {
model.model_info = nullptr; model.model_info = nullptr;
+3
View File
@@ -565,6 +565,9 @@ public:
void export_gcode_3mf(bool export_all = false); void export_gcode_3mf(bool export_all = false);
void send_gcode_finish(wxString name); void send_gcode_finish(wxString name);
void export_core_3mf(); void export_core_3mf();
// Write the project to output_path as a copy: the project's file name, dirty state and model stay
// as they are, and the signed-in account is not added as the designer. False when the write fails.
bool export_3mf_copy(const boost::filesystem::path& output_path);
// Export a "published" 3MF embedding the author-selected settings in the file metadata; a // Export a "published" 3MF embedding the author-selected settings in the file metadata; a
// pure export that leaves the in-memory project untouched. // pure export that leaves the in-memory project untouched.
int export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys); int export_published_3mf(const std::vector<std::string>& published_keys, const std::vector<Slic3r::PublishedMaterialEntry>& material_keys);
+3
View File
@@ -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("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("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("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("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("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction"); optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
+36 -12
View File
@@ -1129,6 +1129,19 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name,
if (http_code) if (http_code)
*http_code = result.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 (result.success) {
if (values_map && result.new_updated_time != 0) { if (values_map && result.new_updated_time != 0) {
(*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.new_updated_time); (*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; SyncPushResult result;
result.success = false; result.success = false;
result.http_code = 0; result.http_code = 0;
result.conflict_code = 0;
result.server_deleted = false; result.server_deleted = false;
nlohmann::json body; nlohmann::json body;
@@ -1429,20 +1443,30 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
err_body = json; err_body = json;
if (json.is_null()) { if (json.is_null()) {
result.server_deleted = true; result.server_deleted = true;
} else { } else if (json.is_object()) {
auto& profile_data = json["server_profile"]; result.conflict_code = json.value("code", 0);
result.server_version.id = profile_data.value("id", ""); if (json.contains("server_profile") && !json["server_profile"].is_null()) {
result.server_version.name = profile_data.value("name", ""); auto& profile_data = json["server_profile"];
result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0); 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 (...) {} } catch (...) {}
// Surface the conflict via the http-error callback with the local preset name injected. // Create-path duplicate_profile_uuid (-2) is an idempotent success: the deterministic id
// The raw server body omits the name for tombstone (-3) conflicts (server_profile is null), // already exists, so the caller adopts the returned id. Skip the conflict notification,
// but the GUI needs it to regenerate the deterministic setting_id for a force push. // otherwise every already-imported preset would raise a Pull/Force-push prompt on each launch.
if (!err_body.is_object()) const bool is_create = original_updated_time.empty();
err_body = nlohmann::json::object(); const bool auto_resolved_duplicate = (is_create && result.conflict_code == -2);
err_body["name"] = name; if (!auto_resolved_duplicate) {
invoke_http_error_callback(409, err_body.dump()); // 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; result.error_message = response;
return result; return result;
} }
@@ -94,6 +94,7 @@ struct SyncPullResponse {
struct SyncPushResult { struct SyncPushResult {
bool success; bool success;
int http_code; int http_code;
int conflict_code;
long long new_updated_time; long long new_updated_time;
ProfileUpsert server_version; ProfileUpsert server_version;
bool server_deleted; bool server_deleted;
+127 -1
View File
@@ -1,16 +1,36 @@
#include "PluginHostBindings.hpp" #include "PluginHostBindings.hpp"
#include <pybind11/cast.h>
#include <pybind11/pybind11.h> #include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <libslic3r/Model.hpp> #include <libslic3r/Model.hpp>
#include <libslic3r/PresetBundle.hpp> #include <libslic3r/PresetBundle.hpp>
#include <libslic3r/AppConfig.hpp>
#include <libslic3r/Semver.hpp>
#include <libslic3r_version.h>
#include <slic3r/GUI/BuildCommit.hpp>
#include <slic3r/GUI/DeviceCore/DevManager.h>
#include <slic3r/GUI/DeviceManager.hpp>
#include <slic3r/GUI/GUI.hpp> #include <slic3r/GUI/GUI.hpp>
#include <slic3r/GUI/GUI_App.hpp> #include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/OpenGLManager.hpp>
#include <slic3r/GUI/Plater.hpp> #include <slic3r/GUI/Plater.hpp>
#include <slic3r/Utils/CloudProvider.hpp>
#include <slic3r/Utils/NetworkAgent.hpp>
#include <slic3r/Utils/NetworkAgentFactory.hpp>
#include <slic3r/Utils/bambu_networking.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/optional/optional.hpp>
#include <memory> #include <memory>
#include <stdexcept> #include <stdexcept>
#include <wx/app.h> #include <wx/app.h>
#include <wx/string.h>
#include <wx/thread.h>
#include <string> #include <string>
#include <utility>
namespace py = pybind11; namespace py = pybind11;
@@ -41,6 +61,94 @@ PresetBundle* current_preset_bundle()
return preset_bundle; return preset_bundle;
} }
GUI::GUI_App& current_app()
{
if (wxTheApp == nullptr)
throw std::runtime_error("OrcaSlicer application is not initialized");
return GUI::wxGetApp();
}
// The plater and the device list are only safe to touch from the UI thread, and the 3MF export
// renders thumbnails, which needs the UI thread's GL context.
void require_main_thread(const char* function_name)
{
if (!wxIsMainThread())
throw std::runtime_error(std::string(function_name) + "() must be called from the UI thread");
}
void export_3mf_copy(GUI::Plater& plater, const std::string& path)
{
require_main_thread("export_3mf_copy");
if (!boost::iends_with(path, ".3mf"))
throw py::value_error("export_3mf_copy() needs a path ending in .3mf");
// The thumbnails are rendered by the 3D view, which may not exist yet while plugins load.
if (!GUI::OpenGLManager::get_gl_info().is_detected())
throw std::runtime_error("export_3mf_copy() needs the 3D view, which is not ready yet");
// The file is written from C++, so raise the audit event Python's open(path, "w") raises: the
// plugin gets the same deny list, permissions and prompt as for writing the file itself.
if (PySys_Audit("open", "ssi", path.c_str(), "w", 0) < 0)
throw py::error_already_set();
if (!plater.export_3mf_copy(GUI::into_path(GUI::from_u8(path))))
throw std::runtime_error("Failed to write " + path);
}
py::object gl_info()
{
const GUI::OpenGLManager::GLInfo& info = GUI::OpenGLManager::get_gl_info();
if (!info.is_detected())
return py::none();
py::dict out;
out["vendor"] = info.get_vendor();
out["renderer"] = info.get_renderer();
out["version"] = info.get_version();
out["glsl_version"] = info.get_glsl_version();
out["core_profile"] = info.is_core_profile();
return std::move(out);
}
py::dict app_info()
{
GUI::GUI_App& app = current_app();
// The app config has no lock of its own; the UI thread is where it is written.
require_main_thread("app_info");
py::dict out;
out["version"] = SoftFever_VERSION;
out["build"] = GUI::build_commit_label;
out["mode"] = app.is_editor() ? "editor" : "gcode viewer";
out["language"] = GUI::into_u8(app.current_language_code_safe());
// The app config file is deny-listed for plugins; these are the parts a bug report needs.
const boost::optional<Semver>& config_version = app.last_config_version();
out["app_config_version"] = config_version && config_version->valid() ? config_version->to_string_sf() : std::string();
out["stealth_mode"] = app.app_config != nullptr && app.app_config->get_stealth_mode();
out["is_signed_in"] = app.is_user_login(ORCA_CLOUD_PROVIDER);
out["is_bambu_signed_in"] = app.is_user_login(BBL_CLOUD_PROVIDER);
out["network_plugin_version"] = NetworkAgent::is_network_module_loaded() ? NetworkAgent::get_version() : std::string();
const NetworkLibraryLoadError load_error = NetworkAgent::get_load_error();
out["network_plugin_error"] = load_error.has_error ? load_error.message : std::string();
return out;
}
py::object selected_printer()
{
GUI::GUI_App& app = current_app();
require_main_thread("selected_printer");
DeviceManager* devices = app.getDeviceManager();
MachineObject* machine = devices != nullptr ? devices->get_selected_machine() : nullptr;
if (machine == nullptr)
return py::none();
// Agent, model, link and firmware only: no name, serial, address or access code. Other agents
// fill the model and firmware with placeholders, so those come from Bambu printers only.
const bool bambu = machine->printer_agent_id == BBL_PRINTER_AGENT_ID;
py::dict out;
out["agent"] = machine->printer_agent_id;
out["model_id"] = bambu ? machine->printer_type : std::string();
out["connection"] = machine->connection_type();
out["online"] = machine->is_online();
out["connected"] = machine->is_connected();
out["firmware"] = bambu ? machine->get_ota_version() : std::string();
return std::move(out);
}
} // namespace } // namespace
// Access to the live GUI application: the Plater and the module-level // Access to the live GUI application: the Plater and the module-level
@@ -52,7 +160,15 @@ void host_bindings::register_app(py::module_& host)
.def("model", static_cast<Model& (GUI::Plater::*)()>(&GUI::Plater::model), py::return_value_policy::reference_internal) .def("model", static_cast<Model& (GUI::Plater::*)()>(&GUI::Plater::model), py::return_value_policy::reference_internal)
.def("is_project_dirty", &GUI::Plater::is_project_dirty) .def("is_project_dirty", &GUI::Plater::is_project_dirty)
.def("is_presets_dirty", &GUI::Plater::is_presets_dirty) .def("is_presets_dirty", &GUI::Plater::is_presets_dirty)
.def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture); .def("inside_snapshot_capture", &GUI::Plater::inside_snapshot_capture)
// Path the project was last opened from or saved to; empty while it has never been saved.
.def("project_path", [](GUI::Plater& plater) {
require_main_thread("project_path");
return GUI::into_u8(plater.get_project_filename(".3mf"));
})
.def("export_3mf_copy", &export_3mf_copy, py::arg("path"),
"Write the current project, unsaved changes included, to path as a 3MF copy. The project's "
"file name and saved state are not changed, and the signed-in account is not written as the designer.");
host.def("plater", &current_plater, py::return_value_policy::reference); host.def("plater", &current_plater, py::return_value_policy::reference);
host.def("model", []() -> Model& { host.def("model", []() -> Model& {
@@ -68,6 +184,16 @@ void host_bindings::register_app(py::module_& host)
throw std::runtime_error("OrcaSlicer application is not initialized"); throw std::runtime_error("OrcaSlicer application is not initialized");
return GUI::into_u8(GUI::wxGetApp().current_language_code_safe()); return GUI::into_u8(GUI::wxGetApp().current_language_code_safe());
}); });
host.def("app_info", &app_info,
"The running app: version, build, mode, language, app_config_version (the version that wrote the app config "
"before this run, empty when there was none), stealth_mode, is_signed_in (Orca Cloud), is_bambu_signed_in (Bambu Cloud), network_plugin_version (empty "
"when the Bambu network plugin is not loaded) and network_plugin_error (empty unless it failed to load).");
host.def("gl_info", &gl_info,
"OpenGL vendor, renderer, version, glsl_version and core_profile of the 3D view; None before it has been created.");
host.def("selected_printer", &selected_printer,
"Agent, model_id, connection ('lan' or 'cloud'), online, connected and firmware of the printer selected in the "
"device list; model_id and firmware are empty for non-Bambu agents. None when none is selected. Print hosts "
"configured in the printer preset (OctoPrint and the like) are not in the device list.");
} }
} // namespace Slic3r } // namespace Slic3r
+11
View File
@@ -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() void PluginPages::show_overflow_menu()
{ {
const int visible_slots = std::max(1, m_visible_page_count); const int visible_slots = std::max(1, m_visible_page_count);
+2
View File
@@ -66,6 +66,8 @@ public:
void relayout(); void relayout();
void select_page(const PluginCapabilityId& id);
private: private:
std::shared_ptr<PagesPluginCapability> get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const; std::shared_ptr<PagesPluginCapability> get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const;
bool create_page(const PluginCapabilityId& id); bool create_page(const PluginCapabilityId& id);
+332
View File
@@ -37,6 +37,7 @@
#include "libslic3r/Fill/Fill.hpp" #include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp" #include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Fill/FillGyroid.hpp" #include "libslic3r/Fill/FillGyroid.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Flow.hpp" #include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp" #include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.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_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); 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<double>(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<std::string, std::string, double>({{"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<std::string, std::string>({{"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<TpmsRadialField::Slice> slices;
for (int i = 0; i < 200; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &box});
const TpmsRadialField field(slices, get_extents(box), TpmsAdaptiveMode::Lobes, [] {});
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<Fill> 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<Line> tree(to_lines(to));
double distance = 0.;
for (const Polyline &path : intersection_pl(from, inner))
for (const Point &point : path.equally_spaced_points(scale_(0.2)))
distance = std::max(distance, tree.distance_from_lines<false>(point));
return unscale<double>(distance);
};
// 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<Line> 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<false>(point));
CHECK(unscale<double>(max_distance) <= 0.012);
}
+60
View File
@@ -9,6 +9,7 @@
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/GCode/GCodeProcessor.hpp" #include "libslic3r/GCode/GCodeProcessor.hpp"
#include "libslic3r/Model.hpp" #include "libslic3r/Model.hpp"
#include "libslic3r/Print.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "test_helpers.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); INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1);
CHECK(difference.first == result.lines_ends.end()); 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<size_t> 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);
}
+2
View File
@@ -3,6 +3,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
add_executable(${_TEST_NAME}_tests add_executable(${_TEST_NAME}_tests
${_TEST_NAME}_tests.cpp ${_TEST_NAME}_tests.cpp
test_3mf.cpp test_3mf.cpp
test_amf.cpp
# Round-trip seam metadata and active/dormant volume settings in both formats. # Round-trip seam metadata and active/dormant volume settings in both formats.
test_precise_seam_3mf.cpp test_precise_seam_3mf.cpp
# Pure perimeter extraction is independent of Print/Layer fixtures. # Pure perimeter extraction is independent of Print/Layer fixtures.
@@ -34,6 +35,7 @@ add_executable(${_TEST_NAME}_tests
test_fill_corner_smoothing.cpp test_fill_corner_smoothing.cpp
test_filament_mixer.cpp test_filament_mixer.cpp
test_fill_plane_path.cpp test_fill_plane_path.cpp
test_fill_tpms_adaptive.cpp
test_geometry.cpp test_geometry.cpp
test_multimaterial_segmentation.cpp test_multimaterial_segmentation.cpp
test_placeholder_parser.cpp test_placeholder_parser.cpp
+80
View File
@@ -0,0 +1,80 @@
#include <catch2/catch_test_macros.hpp>
#include "libslic3r/Config.hpp"
#include "libslic3r/Format/AMF.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "test_utils.hpp"
#include <boost/nowide/fstream.hpp>
#include <ios>
#include <string>
using namespace Slic3r;
namespace {
// The smallest AMF the loader accepts: one object holding one volume, a tetrahedron. The metadata
// element of the object is dropped in verbatim, so a test can hand the parser a malformed one.
std::string amf_with_object_metadata(const std::string &object_metadata)
{
return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
"<amf unit=\"millimeter\">\n"
" <object id=\"0\">\n"
" " + object_metadata + "\n"
" <mesh>\n"
" <vertices>\n"
" <vertex><coordinates><x>0</x><y>0</y><z>0</z></coordinates></vertex>\n"
" <vertex><coordinates><x>1</x><y>0</y><z>0</z></coordinates></vertex>\n"
" <vertex><coordinates><x>0</x><y>1</y><z>0</z></coordinates></vertex>\n"
" <vertex><coordinates><x>0</x><y>0</y><z>1</z></coordinates></vertex>\n"
" </vertices>\n"
" <volume>\n"
" <triangle><v1>0</v1><v2>2</v2><v3>1</v3></triangle>\n"
" <triangle><v1>0</v1><v2>1</v2><v3>3</v3></triangle>\n"
" <triangle><v1>0</v1><v2>3</v2><v3>2</v3></triangle>\n"
" <triangle><v1>1</v1><v2>2</v2><v3>3</v3></triangle>\n"
" </volume>\n"
" </mesh>\n"
" </object>\n"
"</amf>\n";
}
void write_file(const std::string &path, const std::string &content)
{
boost::nowide::ofstream f(path, std::ios::binary);
f << content;
}
bool load(const std::string &path, Model &model)
{
DynamicPrintConfig config;
ConfigSubstitutionContext substitutions(ForwardCompatibilitySubstitutionRule::Disable);
return load_amf(path.c_str(), &config, &substitutions, &model, nullptr);
}
} // namespace
TEST_CASE("An AMF object metadata element with no type attribute is rejected", "[AMF]")
{
ScopedTemporaryFile tmp(".amf");
SECTION("with the attribute the file loads")
{
write_file(tmp.string(), amf_with_object_metadata("<metadata type=\"name\">tetra</metadata>"));
Model model;
REQUIRE(load(tmp.string(), model));
CHECK(model.objects.size() == 1);
}
SECTION("without it the load fails instead of reading a null attribute")
{
write_file(tmp.string(), amf_with_object_metadata("<metadata>tetra</metadata>"));
Model model;
CHECK_FALSE(load(tmp.string(), model));
}
}
+292
View File
@@ -0,0 +1,292 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <vector>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
namespace {
ExPolygon rectangle(double x0, double y0, double x1, double y1)
{
return ExPolygon(Points{Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)});
}
// The expolygons stacked in 0.2 mm layers from z = 0 to height.
TpmsRadialField radial_field(const ExPolygons &expolygons, double height, TpmsAdaptiveMode mode = TpmsAdaptiveMode::Lobes)
{
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons});
return TpmsRadialField(slices, get_extents(expolygons), mode, [] {});
}
double radial(const TpmsRadialField &field, const Vec3d &pt)
{
TpmsRadialField::Radials radials;
field.radial(pt, radials);
return radials[0].t;
}
Vec3d center(const TpmsRadialField &field, const Vec3d &pt)
{
TpmsRadialField::Radials radials;
field.radial(pt, radials);
return radials[0].center;
}
// The grid cells are 0.5 mm, so a radial coordinate over 10 mm is accurate to about a twentieth.
constexpr double Tolerance = 0.075;
} // namespace
TEST_CASE("TPMS radial field is zero at the center of a cube and one at its faces", "[FillTpmsAdaptive]")
{
// A 20 mm cube, 10 mm from its center to every face.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
const Vec3d c = center(field, {10., 10., 10.});
CHECK_THAT(c.x(), WithinAbs(10., 0.5));
CHECK_THAT(c.y(), WithinAbs(10., 0.5));
CHECK_THAT(c.z(), WithinAbs(10., 0.5));
CHECK_THAT(radial(field, {10., 10., 10.}), WithinAbs(0., Tolerance));
for (const Vec3d &face : {Vec3d(0., 10., 10.), Vec3d(20., 10., 10.), Vec3d(10., 0., 10.), Vec3d(10., 10., 0.), Vec3d(10., 10., 20.)}) {
CAPTURE(face.x(), face.y(), face.z());
CHECK_THAT(radial(field, face), WithinAbs(1., Tolerance));
}
}
TEST_CASE("TPMS radial field grows linearly from the center of a cube to its faces", "[FillTpmsAdaptive]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
for (double d = 1.; d < 10.; d += 1.) {
CAPTURE(d);
CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance));
CHECK_THAT(radial(field, {10., 10., 10. + d}), WithinAbs(d / 10., Tolerance));
}
}
TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60.);
CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5));
CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(field, {10., 10., 45.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance));
}
TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTpmsAdaptive]")
{
const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)};
const TpmsRadialField field = radial_field(squares, 20.);
CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5));
CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5));
CHECK_THAT(radial(field, {40., 10., 10.}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance));
}
TEST_CASE("TPMS radial field is beyond one outside of the object", "[FillTpmsAdaptive]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
CHECK(radial(field, {-5., 10., 10.}) > 1.);
CHECK(radial(field, {10., 10., 30.}) > 1.);
}
TEST_CASE("TPMS radial field grades every lobe of a body towards its own center", "[FillTpmsAdaptive]")
{
// Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep.
const Vec3d c1(10., 10., 10.), c2(26., 10., 10.);
std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
for (const Vec3d &c : {c1, c2}) {
Polygon &circle = circles.emplace_back();
for (int k = 0; k < 90; ++k)
circle.points.push_back(Point::new_scale(c.x() + r * std::cos(k * 2. * PI / 90.), c.y() + r * std::sin(k * 2. * PI / 90.)));
}
layers.push_back(union_ex(circles));
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
for (const Vec3d &c : {c1, c2}) {
CAPTURE(c.x());
CHECK_THAT(center(field, c).x(), WithinAbs(c.x(), 0.5));
CHECK_THAT(radial(field, c), WithinAbs(0., Tolerance));
CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance));
}
// The side between the lobes is half way to the surface, where both patterns morph into each other.
TpmsRadialField::Radials radials;
REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2);
for (size_t i = 0; i < 2; ++i) {
CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance));
CHECK_THAT(radials[i].weight, WithinAbs(0.5, 0.05));
}
}
TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously", "[FillTpmsAdaptive]")
{
// Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle.
const std::array<Vec3d, 3> centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)};
std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
for (const Vec3d &c : centers) {
Polygon &circle = circles.emplace_back();
for (int k = 0; k < 90; ++k)
circle.points.push_back(Point::new_scale(c.x() + r * std::cos(k * 2. * PI / 90.), c.y() + r * std::sin(k * 2. * PI / 90.)));
}
layers.push_back(union_ex(circles));
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
// Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly.
const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.;
size_t max_count = 0;
double max_jump = 0.;
double max_error = 0.;
for (int row = 0; row <= 100; ++row) {
std::array<float, 3> previous{};
for (int step = 0; step <= 200; ++step) {
TpmsRadialField::Radials radials;
const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials);
max_count = std::max(max_count, count);
std::array<float, 3> weights{};
for (size_t i = 0; i < count; ++i) {
auto nearest = std::min_element(centers.begin(), centers.end(), [&](const Vec3d &a, const Vec3d &b) {
return (a - radials[i].center).norm() < (b - radials[i].center).norm();
});
weights[nearest - centers.begin()] += radials[i].weight;
}
max_error = std::max(max_error, std::abs(weights[0] + weights[1] + weights[2] - 1.));
if (step > 0)
for (size_t k = 0; k < 3; ++k)
max_jump = std::max(max_jump, double(std::abs(weights[k] - previous[k])));
previous = weights;
}
}
CHECK(max_count == 3);
CHECK(max_error < 1e-5);
CHECK(max_jump < 0.05);
}
TEST_CASE("TPMS radial field is empty when the object is thinner than the grid cells", "[FillTpmsAdaptive]")
{
// A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more.
const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)};
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 1000; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &bar});
const TpmsRadialField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
TpmsAdaptiveMode::Lobes, [] {});
CHECK(field.empty());
}
TEST_CASE("TPMS depth follows the distance to the surface relative to the deepest point", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60., TpmsAdaptiveMode::SmoothBlend);
for (double z : {15., 30., 45.}) {
CAPTURE(z);
CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance));
}
CHECK_THAT(field.depth({5., 10., 30.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(field.depth({10., 10., 55.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance));
}
TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the surface", "[FillTpmsAdaptive]")
{
// In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField cube = radial_field(square, 20., TpmsAdaptiveMode::DistanceWarp);
CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance));
// 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep.
const TpmsRadialField lobes = radial_field(square, 60.);
const TpmsRadialField warp = radial_field(square, 60., TpmsAdaptiveMode::DistanceWarp);
for (double z : {15., 45.}) {
CAPTURE(z);
CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance));
CHECK(radial(warp, {10., 10., z}) < 0.25);
}
CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance));
}
TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards", "[FillTpmsAdaptive]")
{
// The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside.
const ExPolygons square{rectangle(0., 0., 40., 40.)};
const TpmsRadialField field = radial_field(square, 40., TpmsAdaptiveMode::SteppedShells);
const std::vector<TpmsShell> shells = make_tpms_shells(field, square.front(), 20., 0.2f, 0.05f, TpmsAdaptiveGradient::Linear);
REQUIRE(shells.size() == 5);
CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6));
CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6));
double area = 0.;
for (size_t i = 0; i < shells.size(); ++i) {
CAPTURE(i);
if (i > 0)
CHECK(shells[i].density < shells[i - 1].density);
for (const ExPolygon &expolygon : shells[i].expolygons)
area += expolygon.area();
}
CHECK_THAT(area / square.front().area(), WithinAbs(1., 0.01));
const Point center = Point::new_scale(20., 20.);
CHECK(std::any_of(shells.back().expolygons.begin(), shells.back().expolygons.end(),
[&center](const ExPolygon &expolygon) { return expolygon.contains(center); }));
}
TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on its own", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField normal_z = radial_field(square, 60., TpmsAdaptiveMode::NormalZ);
for (double z : {5., 30., 55.}) {
CAPTURE(z);
CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(normal_z, {15., 10., z}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(normal_z, {10., 0., z}), WithinAbs(1., Tolerance));
}
// Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const TpmsRadialField normal_x = radial_field(square, 60., TpmsAdaptiveMode::NormalX);
for (double x : {3., 10., 17.}) {
CAPTURE(x);
CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(normal_x, {x, 10., 45.}), WithinAbs(0.5, Tolerance));
}
}
+12 -1
View File
@@ -133,7 +133,7 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi
{ {
py::object host = import_orca_module().attr("host"); py::object host = import_orca_module().attr("host");
for (const char* function_name : { "preset_bundle", "plater", "model" }) { for (const char* function_name : { "preset_bundle", "plater", "model", "app_info", "selected_printer" }) {
CAPTURE(function_name); CAPTURE(function_name);
try { try {
host.attr(function_name)(); host.attr(function_name)();
@@ -145,6 +145,17 @@ TEST_CASE("Plugin host API reports unavailable GUI objects before Orca app initi
} }
} }
TEST_CASE("Plugin host API reports no GL details before the 3D view exists", "[PluginHost][Python]")
{
py::object host = import_orca_module().attr("host");
CHECK(host.attr("gl_info")().is_none());
py::object plater_type = host.attr("Plater");
CHECK(has_attr(plater_type, "project_path"));
CHECK(has_attr(plater_type, "export_3mf_copy"));
}
TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app initialization", "[PluginHost][Python]") TEST_CASE("Plugin host API exposes the UI module and guards it before Orca app initialization", "[PluginHost][Python]")
{ {
py::object host = import_orca_module().attr("host"); py::object host = import_orca_module().attr("host");