mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-10 17:21:10 +00:00
Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3071d525ee | ||
|
|
b5ef24e7ff | ||
|
|
8585eae816 | ||
|
|
e72ace164b | ||
|
|
b5f5b50157 | ||
|
|
e0b35f9ec9 | ||
|
|
eb28daf0fe | ||
|
|
6ac7ae24a3 | ||
|
|
d55e32fed4 | ||
|
|
cd0b529e31 |
Vendored
+29
@@ -44,6 +44,18 @@ else()
|
||||
if(APPLE)
|
||||
set(_conf_cmd export MACOSX_DEPLOYMENT_TARGET=${CMAKE_OSX_DEPLOYMENT_TARGET} && ./Configure -mmacosx-version-min=${CMAKE_OSX_DEPLOYMENT_TARGET})
|
||||
else()
|
||||
# A static library that is embedded into a shared object must not export
|
||||
# its symbols. On Linux the running process also loads the system OpenSSL
|
||||
# 3.x (WebKitGTK/gnutls pull in libcrypto.so.3), and CPython's _ssl and
|
||||
# _hashlib are dlopened (RTLD_LOCAL) DSOs that each embed this OpenSSL.
|
||||
# With default visibility their unversioned OpenSSL references are
|
||||
# preempted by that global 3.x copy, mixing the 1.1.1 and 3.x ABIs and
|
||||
# corrupting the heap (ssl.create_default_context() aborts). Hidden
|
||||
# visibility makes each embedded copy self-contained. Linux-only: macOS
|
||||
# binds dylibs with a two-level namespace (no interposition) and ships no
|
||||
# OpenSSL, and Windows has no equivalent flag and no system OpenSSL to
|
||||
# collide with.
|
||||
set(_openssl_extra_cflags -fvisibility=hidden)
|
||||
set(_conf_cmd env "CC=${CMAKE_C_COMPILER}" "LDFLAGS=${CMAKE_EXE_LINKER_FLAGS}" "./config")
|
||||
endif()
|
||||
set(_cross_comp_prefix_line "")
|
||||
@@ -102,3 +114,20 @@ ExternalProject_Add_Step(dep_OpenSSL install_cmake_files
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory openssl "${DESTDIR}${CMAKE_INSTALL_LIBDIR}/cmake/openssl"
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIR}"
|
||||
)
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
# OpenSSL's object rules do not depend on CFLAGS, so reconfiguring it (for
|
||||
# example to add -fvisibility=hidden) relinks the archives from stale
|
||||
# objects instead of recompiling them, and the change silently has no
|
||||
# effect. Drop the objects whenever this recipe changes so the next build
|
||||
# actually recompiles them.
|
||||
ExternalProject_Get_Property(dep_OpenSSL SOURCE_DIR)
|
||||
ExternalProject_Add_Step(dep_OpenSSL clean_objects
|
||||
DEPENDEES configure
|
||||
DEPENDERS build
|
||||
COMMAND make clean
|
||||
WORKING_DIRECTORY "${SOURCE_DIR}"
|
||||
DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
|
||||
COMMENT "OpenSSL: cleaning objects after a recipe change"
|
||||
)
|
||||
endif ()
|
||||
|
||||
Vendored
+24
@@ -299,3 +299,27 @@ endif()
|
||||
if(TARGET dep_ZLIB)
|
||||
add_dependencies(dep_python3 dep_ZLIB)
|
||||
endif()
|
||||
|
||||
if (NOT WIN32 AND NOT APPLE)
|
||||
# CPython's Makefile rules for _ssl and _hashlib depend only on their own
|
||||
# sources, not on the OpenSSL archives, so a rebuilt OpenSSL does not make
|
||||
# them relink and they keep the previous symbols. On an incremental tree,
|
||||
# drop the built modules and relink them against the current OpenSSL; a
|
||||
# fresh build is left alone (its PGO target builds them). "make" alone is a
|
||||
# no-op once PGO has run, so sharedmods is invoked explicitly.
|
||||
ExternalProject_Get_Property(dep_python3 SOURCE_DIR)
|
||||
file(GLOB _python_ssl_modules
|
||||
"${SOURCE_DIR}/Modules/_ssl*.so"
|
||||
"${SOURCE_DIR}/Modules/_hashlib*.so")
|
||||
if (_python_ssl_modules)
|
||||
ExternalProject_Add_Step(dep_python3 relink_ssl_extensions
|
||||
DEPENDEES configure
|
||||
DEPENDERS build
|
||||
COMMAND sh -c "rm -f '${SOURCE_DIR}'/Modules/_ssl*.so '${SOURCE_DIR}'/Modules/_hashlib*.so && make -j${NPROC} sharedmods"
|
||||
WORKING_DIRECTORY "${SOURCE_DIR}"
|
||||
COMMENT "CPython: relinking _ssl/_hashlib against the current OpenSSL"
|
||||
DEPENDS "${CMAKE_CURRENT_LIST_FILE}"
|
||||
"${CMAKE_CURRENT_LIST_DIR}/../OpenSSL/OpenSSL.cmake"
|
||||
)
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
@@ -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"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"4000",
|
||||
"4000"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"4000",
|
||||
"4000",
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"4000",
|
||||
"4000"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"4000",
|
||||
"4000"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"4000",
|
||||
"4000",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"4000",
|
||||
"4000",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"30",
|
||||
"30"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"4000",
|
||||
"4000",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"30",
|
||||
"30"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"30",
|
||||
"30"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"50",
|
||||
"50"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"30",
|
||||
"30"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"30",
|
||||
"30"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000"
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"30",
|
||||
"30"
|
||||
],
|
||||
"enable_arc_fitting": "0",
|
||||
"default_acceleration": [
|
||||
"10000",
|
||||
"10000"
|
||||
|
||||
@@ -180,6 +180,8 @@ set(lisbslic3r_sources
|
||||
Fill/FillPlanePath.hpp
|
||||
Fill/FillRectilinear.cpp
|
||||
Fill/FillRectilinear.hpp
|
||||
Fill/FillTpmsAdaptive.cpp
|
||||
Fill/FillTpmsAdaptive.hpp
|
||||
Fill/FillTpmsD.cpp
|
||||
Fill/FillTpmsD.hpp
|
||||
Fill/FillTpmsFK.cpp
|
||||
|
||||
@@ -311,6 +311,11 @@ struct SurfaceFillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" wave.
|
||||
bool gyroid_optimized = false;
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object.
|
||||
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
|
||||
float tpms_interior_density = 0.f;
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
@@ -353,6 +358,9 @@ struct SurfaceFillParams
|
||||
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
|
||||
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
|
||||
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
|
||||
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
|
||||
RETURN_COMPARE_NON_EQUAL(smooth_factor);
|
||||
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
|
||||
RETURN_COMPARE_NON_EQUAL(separated_infills);
|
||||
@@ -386,6 +394,9 @@ struct SurfaceFillParams
|
||||
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
|
||||
this->separated_infills == rhs.separated_infills &&
|
||||
this->gyroid_optimized == rhs.gyroid_optimized &&
|
||||
this->tpms_adaptive == rhs.tpms_adaptive &&
|
||||
this->tpms_interior_density == rhs.tpms_interior_density &&
|
||||
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
|
||||
this->smooth_factor == rhs.smooth_factor &&
|
||||
this->fill_order == rhs.fill_order;
|
||||
}
|
||||
@@ -994,15 +1005,23 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
// Orca: apply fill multiline only for sparse infill
|
||||
params.multiline = params.extrusion_role == erInternalInfill ? int(region_config.fill_multiline) : 1;
|
||||
|
||||
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
|
||||
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
|
||||
// (which would unnecessarily split fill batching).
|
||||
// Stored on SurfaceFillParams; copied to FillParams during conversion.
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized;
|
||||
// Orca: Likewise separated_infills only where it can move the pattern.
|
||||
// Orca: Pass through separated_infills only where it can move the pattern.
|
||||
params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) &&
|
||||
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
|
||||
|
||||
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
|
||||
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
|
||||
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
|
||||
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
|
||||
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
|
||||
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
|
||||
|
||||
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
|
||||
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
|
||||
// (which would unnecessarily split fill batching). Adaptive density replaces it.
|
||||
// Stored on SurfaceFillParams; copied to FillParams during conversion.
|
||||
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
|
||||
|
||||
if (params.extrusion_role == erInternalInfill) {
|
||||
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
|
||||
region_config.sparse_infill_rotate_template.value);
|
||||
@@ -1354,6 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
if (surface_fill.params.pattern == ipConcentricInternal) {
|
||||
@@ -1403,6 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
|
||||
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
|
||||
// BBS
|
||||
@@ -1575,6 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
|
||||
f->angle = surface_fill.params.angle;
|
||||
f->fixed_angle = surface_fill.params.fixed_angle;
|
||||
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
|
||||
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
|
||||
f->print_config = &this->object()->print()->config();
|
||||
f->print_object_config = &this->object()->config();
|
||||
|
||||
@@ -1614,6 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
|
||||
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
|
||||
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
|
||||
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
|
||||
// Without the sparse extrusion role, the filler uses each surface's bounds
|
||||
|
||||
@@ -33,6 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
|
||||
namespace Slic3r {
|
||||
|
||||
class Surface;
|
||||
class TpmsRadialField;
|
||||
enum InfillPattern : int;
|
||||
|
||||
namespace FillAdaptive {
|
||||
@@ -91,6 +92,11 @@ struct FillParams
|
||||
// For Gyroid: when true, use the parameterized "optimized" variant.
|
||||
bool gyroid_optimized { false };
|
||||
|
||||
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
|
||||
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
|
||||
float tpms_interior_density { 0.f };
|
||||
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
|
||||
|
||||
// Orca: corner smoothing factor in the range [0, 1].
|
||||
double smooth_factor { 0. };
|
||||
|
||||
@@ -155,6 +161,9 @@ public:
|
||||
// Octree builds on mesh for usage in the adaptive cubic infill
|
||||
FillAdaptive::Octree* adapt_fill_octree = nullptr;
|
||||
|
||||
// Radial coordinate inside the object for the adaptive TPMS infill
|
||||
const TpmsRadialField* tpms_radial_field = nullptr;
|
||||
|
||||
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
|
||||
// Orca: also used by gap fill function.
|
||||
const PrintConfig *print_config = nullptr;
|
||||
|
||||
@@ -17,6 +17,17 @@
|
||||
#include "libslic3r/Polyline.hpp"
|
||||
#include "FillGyroid.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
static float gyroid(float x, float y, float z)
|
||||
{
|
||||
return std::sin(x) * std::cos(y) + std::sin(y) * std::cos(z) + std::sin(z) * std::cos(x);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Marching-squares scalar field for the optimized gyroid branch.
|
||||
@@ -62,10 +73,7 @@ struct GyroidField
|
||||
|
||||
float get_scalar(coordf_t x, coordf_t y, coordf_t z_arg) const
|
||||
{
|
||||
const float a = fx * float(x);
|
||||
const float b = fy * float(y);
|
||||
const float c = fz * float(z_arg);
|
||||
return std::sin(a) * std::cos(b) + std::sin(b) * std::cos(c) + std::sin(c) * std::cos(a);
|
||||
return gyroid(fx * float(x), fy * float(y), fz * float(z_arg));
|
||||
}
|
||||
|
||||
float get_scalar(Coord p) const
|
||||
@@ -307,6 +315,14 @@ void FillGyroid::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
|
||||
if(std::abs(infill_angle) >= EPSILON)
|
||||
expolygon.rotate(-infill_angle);
|
||||
@@ -326,7 +342,12 @@ void FillGyroid::_fill_surface_single(
|
||||
|
||||
// generate pattern
|
||||
Polylines polylines;
|
||||
if (params.gyroid_optimized) {
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else if (params.gyroid_optimized) {
|
||||
// Marching-squares path on the gyroid implicit field. Base period matches
|
||||
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
|
||||
// omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
|
||||
|
||||
@@ -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 ¢er) const
|
||||
{
|
||||
Vec3d d = pt - center;
|
||||
if (m_axis >= 0)
|
||||
d[m_axis] = 0.;
|
||||
return d;
|
||||
}
|
||||
|
||||
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
|
||||
{
|
||||
const Vec3d d = this->offset(pt, lobe.center);
|
||||
const double r = d.norm();
|
||||
// Distance warp: the radial coordinate of the linear profile with the same mean depth, which is 1 - 3/4 of it.
|
||||
auto warp = [](double mean_depth) { return std::max(0., 4. / 3. * (1. - mean_depth)); };
|
||||
if (r < EPSILON)
|
||||
return lobe.mean_depth.empty() ? 0. : warp(lobe.mean_depth.front());
|
||||
int i = 0;
|
||||
double fi = 0.;
|
||||
double azimuth;
|
||||
if (m_axis < 0) {
|
||||
const double polar = std::clamp(std::acos(std::clamp(d.z() / r, -1., 1.)) / PI * Polar - 0.5, 0., double(Polar - 1));
|
||||
i = std::min(int(polar), Polar - 2);
|
||||
fi = polar - i;
|
||||
azimuth = std::atan2(d.y(), d.x());
|
||||
} else
|
||||
azimuth = std::atan2(d[(m_axis + 2) % 3], d[(m_axis + 1) % 3]);
|
||||
azimuth *= Azimuth / (2. * PI);
|
||||
if (azimuth < 0.)
|
||||
azimuth += Azimuth;
|
||||
const int j0 = int(azimuth) % Azimuth;
|
||||
const int j1 = (j0 + 1) % Azimuth;
|
||||
const double fj = azimuth - std::floor(azimuth);
|
||||
auto at = [&lobe](int i, int j) { return double(lobe.reach[i * Azimuth + j]); };
|
||||
double reach = at(i, j0) * (1. - fj) + at(i, j1) * fj;
|
||||
if (fi > 0.)
|
||||
reach = reach * (1. - fi) + (at(i + 1, j0) * (1. - fj) + at(i + 1, j1) * fj) * fi;
|
||||
const double tau = r / reach;
|
||||
if (lobe.mean_depth.empty())
|
||||
return tau;
|
||||
// Beyond the surface, the depth is zero.
|
||||
auto mean_at = [&lobe, tau](int i, int j) {
|
||||
const float *mean = lobe.mean_depth.data() + size_t(i * Azimuth + j) * (Samples + 1);
|
||||
if (tau >= 1.)
|
||||
return double(mean[Samples]) / (tau * tau * tau);
|
||||
const double x = tau * Samples;
|
||||
const int k = std::min(int(x), Samples - 1);
|
||||
return mean[k] + (mean[k + 1] - mean[k]) * (x - k);
|
||||
};
|
||||
double mean = mean_at(i, j0) * (1. - fj) + mean_at(i, j1) * fj;
|
||||
if (fi > 0.)
|
||||
mean = mean * (1. - fi) + (mean_at(i + 1, j0) * (1. - fj) + mean_at(i + 1, j1) * fj) * fi;
|
||||
return warp(mean);
|
||||
}
|
||||
|
||||
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
|
||||
{
|
||||
assert(!this->empty());
|
||||
Vec3i32 idx;
|
||||
for (int axis = 0; axis < 3; ++axis)
|
||||
idx[axis] = std::clamp<int>(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 ¶ms, 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
|
||||
@@ -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 ¢er) const;
|
||||
int directions() const { return m_axis < 0 ? Polar * Azimuth : Azimuth; }
|
||||
|
||||
// Directions of the reach of a lobe, on a latitude-longitude grid, or a circle in the 2D modes.
|
||||
static constexpr int Polar = 24;
|
||||
static constexpr int Azimuth = 48;
|
||||
|
||||
TpmsAdaptiveMode m_mode;
|
||||
int m_axis;
|
||||
Vec3d m_origin;
|
||||
double m_cell;
|
||||
Vec3i32 m_size;
|
||||
// Nearest body of every grid node.
|
||||
std::vector<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 ¶ms, coordf_t spacing,
|
||||
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
|
||||
|
||||
} // namespace Slic3r
|
||||
@@ -14,8 +14,10 @@
|
||||
#include "libslic3r/Fill/FillBase.hpp"
|
||||
#include "libslic3r/Point.hpp"
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
#include "libslic3r/libslic3r.h"
|
||||
#include "FillTpmsD.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
@@ -23,6 +25,11 @@ static double scaled_floor(double x,double scale){
|
||||
return std::floor(x/scale)*scale;
|
||||
}
|
||||
|
||||
static float schwarz_d(float x, float y, float z)
|
||||
{
|
||||
return std::sin(x) * std::sin(y) * std::sin(z) - std::cos(x) * std::cos(y) * std::cos(z);
|
||||
}
|
||||
|
||||
static Polylines make_waves(double gridZ, double density_adjusted, double line_spacing, double width, double height)
|
||||
{
|
||||
const double scaleFactor = scale_(line_spacing) / density_adjusted;
|
||||
@@ -110,31 +117,49 @@ void FillTpmsD::_fill_surface_single(
|
||||
ExPolygon expolygon,
|
||||
Polylines &polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto infill_angle = float(this->angle + (CorrectionAngle * 2*M_PI) / 360.);
|
||||
if(std::abs(infill_angle) >= EPSILON)
|
||||
expolygon.rotate(-infill_angle);
|
||||
|
||||
BoundingBox bb = expolygon.contour.bounding_box();
|
||||
// Density adjusted to have a good %of weight.
|
||||
double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
|
||||
// Distance between the gyroid waves in scaled coordinates.
|
||||
coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
// Radians per mm of the regular pattern at a density.
|
||||
auto frequency = [¶ms, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
|
||||
BoundingBox bbox = expolygon.contour.bounding_box();
|
||||
bbox.offset(scale_((params.multiline + 1) * this->spacing));
|
||||
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
|
||||
} else {
|
||||
BoundingBox bb = expolygon.contour.bounding_box();
|
||||
// Density adjusted to have a good %of weight.
|
||||
double density_adjusted = std::max(0., params.density * DensityAdjust / params.multiline);
|
||||
// Distance between the gyroid waves in scaled coordinates.
|
||||
coord_t distance = coord_t(scale_(this->spacing) / density_adjusted);
|
||||
|
||||
// align bounding box to a multiple of our grid module
|
||||
bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
|
||||
// align bounding box to a multiple of our grid module
|
||||
bb.merge(align_to_grid(bb.min, Point(2*M_PI*distance, 2*M_PI*distance)));
|
||||
|
||||
// generate pattern
|
||||
Polylines polylines = make_waves(
|
||||
scale_(this->z),
|
||||
density_adjusted,
|
||||
this->spacing,
|
||||
ceil(bb.size()(0) / distance) + 1.,
|
||||
ceil(bb.size()(1) / distance) + 1.);
|
||||
// generate pattern
|
||||
polylines = make_waves(
|
||||
scale_(this->z),
|
||||
density_adjusted,
|
||||
this->spacing,
|
||||
ceil(bb.size()(0) / distance) + 1.,
|
||||
ceil(bb.size()(1) / distance) + 1.);
|
||||
|
||||
// shift the polyline to the grid origin
|
||||
for (Polyline &pl : polylines)
|
||||
pl.translate(bb.min);
|
||||
}
|
||||
|
||||
// shift the polyline to the grid origin
|
||||
for (Polyline &pl : polylines)
|
||||
pl.translate(bb.min);
|
||||
|
||||
// Apply multiline offset if needed
|
||||
multiline_fill(polylines, params, spacing);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include "libslic3r/Fill/FillBase.hpp"
|
||||
#include "libslic3r/ExPolygon.hpp"
|
||||
#include "FillTpmsFK.hpp"
|
||||
#include "FillTpmsAdaptive.hpp"
|
||||
#include <cmath>
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
@@ -17,6 +18,18 @@
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include "libslic3r/Polygon.hpp"
|
||||
#include "libslic3r/PrintConfig.hpp"
|
||||
|
||||
namespace Slic3r {
|
||||
|
||||
// Fischer - Koch S equation:
|
||||
// cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
|
||||
static float fischer_koch(float x, float y, float z)
|
||||
{
|
||||
return cosf(2 * x) * sinf(y) * cosf(z) + cosf(2 * y) * sinf(z) * cosf(x) + cosf(2 * z) * sinf(x) * cosf(y);
|
||||
}
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
namespace marchsq {
|
||||
using namespace Slic3r;
|
||||
@@ -42,16 +55,7 @@ struct ScalarField
|
||||
{}
|
||||
|
||||
// Get the scalar field value at x,y,z in coordf_t coordinates.
|
||||
float get_scalar(coordf_t x, coordf_t y, coordf_t z) const
|
||||
{
|
||||
const float fx = freq * x;
|
||||
const float fy = freq * y;
|
||||
const float fz = freq * z;
|
||||
|
||||
// Fischer - Koch S equation:
|
||||
// cos(2x)sin(y)cos(z) + cos(2y)sin(z)cos(x) + cos(2z)sin(x)cos(y) = 0
|
||||
return cosf(2 * fx) * sinf(fy) * cosf(fz) + cosf(2 * fy) * sinf(fz) * cosf(fx) + cosf(2 * fz) * sinf(fx) * cosf(fy);
|
||||
}
|
||||
float get_scalar(coordf_t x, coordf_t y, coordf_t z) const { return fischer_koch(freq * x, freq * y, freq * z); }
|
||||
|
||||
// Get the scalar field value at a Coord for the current z value.
|
||||
float get_scalar(Coord p) const
|
||||
@@ -128,20 +132,34 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
|
||||
ExPolygon expolygon,
|
||||
Polylines& polylines_out)
|
||||
{
|
||||
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
|
||||
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
|
||||
[&](const FillParams &shell_params, const ExPolygon &shell) {
|
||||
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
auto infill_angle = float(this->angle + (CorrectionAngle * 2 * M_PI) / 360.);
|
||||
if (std::abs(infill_angle) >= EPSILON)
|
||||
expolygon.rotate(-infill_angle);
|
||||
|
||||
float density_factor = std::min(0.9f, params.density);
|
||||
// Density (field period) adjusted to have a good %of weight.
|
||||
const float vari_T = 4.18f * spacing * params.multiline / density_factor;
|
||||
auto period = [¶ms, this](float density) { return 4.18f * spacing * params.multiline / std::min(0.9f, density); };
|
||||
|
||||
BoundingBox bbox = expolygon.contour.bounding_box();
|
||||
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
|
||||
bbox.offset(scale_((params.multiline + 1) * spacing));
|
||||
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, vari_T);
|
||||
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
|
||||
Polylines polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
|
||||
Polylines polylines;
|
||||
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
|
||||
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
|
||||
params.tpms_adaptive_gradient},
|
||||
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
|
||||
} else {
|
||||
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
|
||||
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
|
||||
polylines = marchsq::get_polylines(sf, SCALED_SPARSE_INFILL_RESOLUTION);
|
||||
}
|
||||
|
||||
// Apply multiline offset if needed
|
||||
multiline_fill(polylines, params, spacing);
|
||||
|
||||
@@ -314,8 +314,13 @@ void AMFParserContext::startElement(const char *name, const char **atts)
|
||||
case 2:
|
||||
if (strcmp(name, "metadata") == 0) {
|
||||
if (m_path[1] == NODE_TYPE_MATERIAL || m_path[1] == NODE_TYPE_OBJECT) {
|
||||
m_value[0] = get_attribute(atts, "type");
|
||||
node_type_new = NODE_TYPE_METADATA;
|
||||
const char *type = get_attribute(atts, "type");
|
||||
if (type == nullptr)
|
||||
this->stop();
|
||||
else {
|
||||
m_value[0] = type;
|
||||
node_type_new = NODE_TYPE_METADATA;
|
||||
}
|
||||
}
|
||||
}/* else if (strcmp(name, "layer_config_ranges") == 0 && m_path[1] == NODE_TYPE_OBJECT)
|
||||
node_type_new = NODE_TYPE_LAYER_CONFIG;*/
|
||||
|
||||
@@ -437,10 +437,14 @@ coordf_t Layer::get_sparse_infill_max_void_area()
|
||||
double max_void_area = 0.;
|
||||
for (auto layerm : m_regions) {
|
||||
Flow flow = layerm->flow(frInfill);
|
||||
float density = layerm->region().config().sparse_infill_density;
|
||||
InfillPattern pattern = layerm->region().config().sparse_infill_pattern;
|
||||
const PrintRegionConfig &config = layerm->region().config();
|
||||
float density = config.sparse_infill_density;
|
||||
InfillPattern pattern = config.sparse_infill_pattern;
|
||||
if (density == 0.)
|
||||
return -1;
|
||||
// Orca: the adaptive TPMS infill is as sparse as its interior density.
|
||||
if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern))
|
||||
density = std::min(density, std::max(1.f, float(config.tpms_interior_density)));
|
||||
|
||||
//BBS: rough estimation and need to be optimized
|
||||
double spacing = flow.scaled_spacing() * (100 - density) / density;
|
||||
|
||||
@@ -1181,6 +1181,9 @@ static std::vector<std::string> s_Preset_print_options{
|
||||
"is_infill_first",
|
||||
"sparse_infill_density",
|
||||
"fill_multiline",
|
||||
"tpms_adaptive",
|
||||
"tpms_interior_density",
|
||||
"tpms_adaptive_gradient",
|
||||
"gyroid_optimized",
|
||||
"sparse_infill_pattern",
|
||||
"sparse_infill_smooth_factor",
|
||||
@@ -2345,7 +2348,7 @@ bool PresetCollection::reset_project_embedded_presets()
|
||||
return re_select;
|
||||
}
|
||||
|
||||
void PresetCollection::set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time)
|
||||
void PresetCollection::set_sync_info_and_save(std::string name, std::string setting_id, std::string syncinfo, long long update_time, const std::string& user_id)
|
||||
{
|
||||
lock();
|
||||
const std::string canonical_name = this->canonical_preset_name(name);
|
||||
@@ -2363,7 +2366,10 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
|
||||
preset2.save_info();
|
||||
}
|
||||
}
|
||||
preset->setting_id = setting_id;
|
||||
if (!setting_id.empty())
|
||||
preset->setting_id = setting_id;
|
||||
if (!user_id.empty())
|
||||
preset->user_id = user_id;
|
||||
if (update_time > 0)
|
||||
preset->updated_time = update_time;
|
||||
if (preset->sync_info == "update")
|
||||
@@ -2653,6 +2659,9 @@ bool PresetCollection::load_user_preset(std::string name, std::map<std::string,
|
||||
iter->base_id = based_id;
|
||||
iter->filament_id = cloud_filament_id;
|
||||
update_alias(*iter);
|
||||
// Persist the cloud-assigned identity to disk, mirroring the equal/newer branch
|
||||
// above; otherwise the id stays only in memory and the next launch rewrites it.
|
||||
iter->save_info();
|
||||
//presets_loaded.emplace_back(*it->second);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(", update the user preset %1% from cloud, type %2%, setting_id %3%, base_id %4%, sync_info %5% inherits %6%, filament_id %7%")
|
||||
% iter->name %Preset::get_type_string(m_type) %iter->setting_id %iter->base_id %iter->sync_info %iter->inherits() % iter->filament_id;
|
||||
|
||||
@@ -603,7 +603,7 @@ public:
|
||||
void update_after_user_presets_loaded();
|
||||
//BBS: get user 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);
|
||||
|
||||
//BBS: add function to generate differed preset for save
|
||||
|
||||
@@ -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
|
||||
{
|
||||
if (config().skirt_loops == 0 || config().skirt_type != stPerObject || m_objects.empty())
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "PrintBase.hpp"
|
||||
#include "Fill/FillAdaptive.hpp"
|
||||
#include "Fill/FillLightning.hpp"
|
||||
#include "Fill/FillTpmsAdaptive.hpp"
|
||||
|
||||
#include "BoundingBox.hpp"
|
||||
#include "ExtrusionEntityCollection.hpp"
|
||||
@@ -406,6 +407,7 @@ public:
|
||||
double max_z() const { return m_max_z; }
|
||||
// Centering offset of the sliced mesh from the scaled and rotated mesh of the model.
|
||||
const Point& center_offset() const { return m_center_offset; }
|
||||
const TpmsRadialField* tpms_radial_field(TpmsAdaptiveMode mode) const { return m_tpms_radial_fields[size_t(mode)].get(); }
|
||||
|
||||
// BBS
|
||||
void generate_support_preview();
|
||||
@@ -650,6 +652,7 @@ private:
|
||||
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
|
||||
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
|
||||
FillLightning::GeneratorPtr prepare_lightning_infill_data();
|
||||
TpmsRadialFields prepare_tpms_radial_fields() const;
|
||||
|
||||
// BBS
|
||||
SupportNecessaryType is_support_necessary();
|
||||
@@ -700,6 +703,7 @@ private:
|
||||
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
|
||||
std::vector<BoundingBox> m_separated_body_bboxes;
|
||||
FillLightning::GeneratorPtr m_lightning_generator;
|
||||
TpmsRadialFields m_tpms_radial_fields;
|
||||
|
||||
std::vector < VolumeSlices > firstLayerObjSliceByVolume;
|
||||
std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups;
|
||||
@@ -1287,6 +1291,11 @@ public:
|
||||
void set_gcode_file_ready();
|
||||
void set_gcode_file_invalidated();
|
||||
void export_gcode_from_previous_file(const std::string& file, GCodeProcessorResult* result, ThumbnailsGeneratorCallback thumbnail_cb = nullptr);
|
||||
// Re-reads the moves and line offsets of `result` from its G-code file after the file was rewritten in
|
||||
// place (post-processing scripts or plugins), so the preview and its G-code window follow the file on
|
||||
// disk. Everything else in `result` was computed while slicing and is kept. If the file cannot be
|
||||
// re-read, the moves are kept and the line offsets are cleared, which hides the G-code window.
|
||||
void reload_gcode_moves(GCodeProcessorResult* result) const;
|
||||
//BBS: add modify_count logic
|
||||
int get_modified_count() const {return m_modified_count;}
|
||||
//BBS: add status for whether support used
|
||||
|
||||
@@ -364,6 +364,26 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder)
|
||||
|
||||
//Orca
|
||||
static t_config_enum_values s_keys_map_TpmsAdaptiveMode{
|
||||
{ "disabled", int(TpmsAdaptiveMode::Disabled) },
|
||||
{ "distance_warp", int(TpmsAdaptiveMode::DistanceWarp) },
|
||||
{ "smooth_blend", int(TpmsAdaptiveMode::SmoothBlend) },
|
||||
{ "stepped_shells", int(TpmsAdaptiveMode::SteppedShells) },
|
||||
{ "lobes", int(TpmsAdaptiveMode::Lobes) },
|
||||
{ "normal_z", int(TpmsAdaptiveMode::NormalZ) },
|
||||
{ "normal_y", int(TpmsAdaptiveMode::NormalY) },
|
||||
{ "normal_x", int(TpmsAdaptiveMode::NormalX) },
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode)
|
||||
|
||||
static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{
|
||||
{ "linear", int(TpmsAdaptiveGradient::Linear) },
|
||||
{ "quadratic", int(TpmsAdaptiveGradient::Quadratic) },
|
||||
{ "exponential", int(TpmsAdaptiveGradient::Exponential) },
|
||||
};
|
||||
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient)
|
||||
|
||||
//BBS
|
||||
static t_config_enum_values s_keys_map_PrintSequence {
|
||||
{ "by layer", int(PrintSequence::ByLayer) },
|
||||
@@ -3620,6 +3640,73 @@ void PrintConfigDef::init_fff_params()
|
||||
def->max = 10; // Maximum number of lines for infill pattern
|
||||
def->set_default_value(new ConfigOptionInt(1));
|
||||
|
||||
def = this->add("tpms_adaptive", coEnum);
|
||||
def->label = L("Adaptive density (experimental)");
|
||||
def->category = L("Strength");
|
||||
def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the "
|
||||
"object towards its center. The sparse infill density is used at the surface and the interior "
|
||||
"density at the center.\n"
|
||||
"Distance warp, Smooth blend and Stepped shells follow the distance to the nearest surface, "
|
||||
"including the top and bottom, with the interior density at the point farthest from it:\n"
|
||||
" - Distance warp: one continuous pattern, stretched and sheared where the distance changes "
|
||||
"across directions, as in plates and long parts.\n"
|
||||
" - Smooth blend: the patterns of neighbouring densities blended into each other, with small "
|
||||
"loops where they meet.\n"
|
||||
" - Stepped shells: shells of the regular pattern at densities about 1.5 times apart, their "
|
||||
"lines joined along the shell boundaries.\n"
|
||||
" - Lobes: follows the 3D shape of the object, including its top and bottom. Every lobe, a part "
|
||||
"joined to the rest by a narrower neck, is graded towards its own center.\n"
|
||||
" - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density "
|
||||
"does not change along it.");
|
||||
def->enum_keys_map = &ConfigOptionEnum<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
|
||||
// (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
|
||||
|
||||
@@ -172,6 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
|
||||
|
||||
// Orca: Infill patterns laid out by an octree, which each connected body always gets of its own.
|
||||
inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; }
|
||||
// Orca: Infill patterns graded by the "tpms_adaptive" option.
|
||||
inline bool is_tpms_adaptive_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
|
||||
|
||||
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
|
||||
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
|
||||
@@ -257,6 +259,26 @@ enum class SurfaceFillOrder {
|
||||
Count,
|
||||
};
|
||||
|
||||
// Orca: what the adaptive TPMS density follows: the 3D shape of the object, or its 2D sections normal to an axis.
|
||||
enum class TpmsAdaptiveMode {
|
||||
Disabled,
|
||||
DistanceWarp,
|
||||
SmoothBlend,
|
||||
SteppedShells,
|
||||
Lobes,
|
||||
NormalZ,
|
||||
NormalY,
|
||||
NormalX,
|
||||
Count,
|
||||
};
|
||||
|
||||
// Orca: how the adaptive TPMS density changes from the object surface to its deepest point.
|
||||
enum class TpmsAdaptiveGradient {
|
||||
Linear,
|
||||
Quadratic,
|
||||
Exponential,
|
||||
};
|
||||
|
||||
//BBS
|
||||
enum class PrintSequence {
|
||||
ByLayer,
|
||||
@@ -774,6 +796,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveMode)
|
||||
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveGradient)
|
||||
|
||||
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
|
||||
|
||||
@@ -1450,6 +1474,9 @@ PRINT_CONFIG_CLASS_DEFINE(
|
||||
// Orca:
|
||||
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
|
||||
((ConfigOptionInt, fill_multiline))
|
||||
((ConfigOptionEnum<TpmsAdaptiveMode>, tpms_adaptive))
|
||||
((ConfigOptionPercent, tpms_interior_density))
|
||||
((ConfigOptionEnum<TpmsAdaptiveGradient>, tpms_adaptive_gradient))
|
||||
((ConfigOptionBool, gyroid_optimized))
|
||||
// Ironing options
|
||||
((ConfigOptionEnum<IroningType>, ironing_type))
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
#include "Fill/FillAdaptive.hpp"
|
||||
#include "Fill/Fill.hpp"
|
||||
#include "Fill/FillLightning.hpp"
|
||||
#include "Fill/FillTpmsAdaptive.hpp"
|
||||
#include "format.hpp"
|
||||
#include "AABBTreeIndirect.hpp"
|
||||
#include "AABBTreeLines.hpp"
|
||||
@@ -1297,6 +1298,38 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
|
||||
return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr();
|
||||
}
|
||||
|
||||
TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
|
||||
{
|
||||
TpmsRadialFields fields;
|
||||
std::array<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()
|
||||
{
|
||||
if (!m_shared_object) {
|
||||
@@ -1700,6 +1733,9 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_overhang_angle") {
|
||||
steps.emplace_back(posInfill);
|
||||
} else if (opt_key == "sparse_infill_pattern"
|
||||
|| opt_key == "tpms_adaptive"
|
||||
|| opt_key == "tpms_interior_density"
|
||||
|| opt_key == "tpms_adaptive_gradient"
|
||||
// Orca: Body centering now also determines bridge anchors during preparation.
|
||||
// Invalidating preparation also invalidates infill, including top/bottom surfaces.
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
@@ -3207,6 +3243,7 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
|
||||
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
|
||||
this->m_tpms_radial_fields = this->prepare_tpms_radial_fields();
|
||||
|
||||
std::vector<size_t> layers_to_generate_infill;
|
||||
for (const auto &pair : surfaces_by_layer) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
// 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) ------
|
||||
|
||||
constexpr const char* kSettingPrefix = "orca_setting";
|
||||
@@ -311,13 +361,20 @@ void ActionRegistry::init()
|
||||
});
|
||||
};
|
||||
auto on_capability = [this](const PluginCapabilityId& capability, ActionChange change) {
|
||||
if (capability.type != PluginCapabilityType::Script || !wxTheApp || wxGetApp().is_closing())
|
||||
if (capability.type != PluginCapabilityType::Script && capability.type != PluginCapabilityType::Pages)
|
||||
return;
|
||||
const std::string plugin_key = capability.plugin_key;
|
||||
const std::string name = capability.name;
|
||||
wxGetApp().CallAfter([this, plugin_key, name, change] {
|
||||
if (!wxGetApp().is_closing())
|
||||
if (!wxTheApp || wxGetApp().is_closing())
|
||||
return;
|
||||
const PluginCapabilityType type = capability.type;
|
||||
const std::string plugin_key = capability.plugin_key;
|
||||
const std::string name = capability.name;
|
||||
wxGetApp().CallAfter([this, type, plugin_key, name, change] {
|
||||
if (wxGetApp().is_closing())
|
||||
return;
|
||||
if (type == PluginCapabilityType::Script)
|
||||
this->refresh_capability(plugin_key, name, change);
|
||||
else
|
||||
this->refresh_page_capability(plugin_key, name, change);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -347,6 +404,16 @@ void ActionRegistry::init()
|
||||
upsert(std::move(action));
|
||||
}
|
||||
|
||||
for (const auto& capability : manager.get_plugin_capabilities("", PluginCapabilityType::Pages)) {
|
||||
if (!capability)
|
||||
continue;
|
||||
const std::string& key = capability->audit_plugin_key();
|
||||
auto it = source_names.find(key);
|
||||
const std::string& source_name = it == source_names.end() ? key : it->second;
|
||||
if (auto action = make_page_action(key, capability->name(), source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
|
||||
// Built-in palette commands (Save/Load, Preferences, Mode switch, Slice/Preview, Go to layer).
|
||||
// Register after plugins so the plugin ids win on any (unlikely) id collision - ids are distinct
|
||||
// by prefix, so this is order-independent. The catalog (and its thin AppAction adapter) lives in
|
||||
@@ -401,6 +468,12 @@ void ActionRegistry::refresh_source(const std::string& plugin_key, ActionChange
|
||||
if (auto action = make_action(plugin_key, capability->name(), source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
for (const auto& capability : manager.get_plugin_capabilities(plugin_key, PluginCapabilityType::Pages)) {
|
||||
if (!capability)
|
||||
continue;
|
||||
if (auto action = make_page_action(plugin_key, capability->name(), source_name))
|
||||
upsert(std::move(action));
|
||||
}
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
|
||||
@@ -420,6 +493,23 @@ void ActionRegistry::refresh_capability(const std::string& plugin_key, const std
|
||||
remove(id);
|
||||
}
|
||||
|
||||
void ActionRegistry::refresh_page_capability(const std::string& plugin_key, const std::string& capability, ActionChange change)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
const std::string id = PluginPageAction::id_for(plugin_key, capability);
|
||||
if (change == ActionChange::Removed) {
|
||||
remove(id);
|
||||
return;
|
||||
}
|
||||
|
||||
PluginManager& manager = PluginManager::instance();
|
||||
if (auto action = make_page_action(plugin_key, capability, find_loaded_source_name(manager, plugin_key)))
|
||||
upsert(std::move(action));
|
||||
else
|
||||
remove(id);
|
||||
}
|
||||
|
||||
void ActionRegistry::upsert(std::unique_ptr<AppAction> action)
|
||||
{
|
||||
assert(wxThread::IsMain());
|
||||
|
||||
@@ -239,6 +239,7 @@ private:
|
||||
// one plugin's whole action set; refresh_capability touches a single capability.
|
||||
void refresh_source(const std::string& plugin_key, ActionChange change);
|
||||
void refresh_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
|
||||
void refresh_page_capability(const std::string& plugin_key, const std::string& capability, ActionChange change);
|
||||
|
||||
bool m_started = false; // init() runs exactly once; guards double-subscription
|
||||
std::unordered_map<std::string, std::shared_ptr<AppAction>> m_actions; // UI-thread confined; no lock
|
||||
|
||||
@@ -283,11 +283,12 @@ void BackgroundSlicingProcess::process_fff()
|
||||
m_temp_output_path = this->get_current_plate()->get_tmp_gcode_path();
|
||||
m_fff_print->export_gcode(m_temp_output_path, m_gcode_result,
|
||||
[this](const ThumbnailsParams& params) { return this->render_thumbnails(params); });
|
||||
// Orca: BBL printers post-process the g-code in place here and never re-parse it into a fresh
|
||||
// GCodeProcessorResult, so m_gcode_result->nozzle_group_result (consumed by the H2C print-dispatch
|
||||
// nozzle mapping) survives post-processing. No preservation guard is needed on this path.
|
||||
// Orca: BBL printers post-process the g-code in place here, in the file the G-code viewer maps, so
|
||||
// the preview re-reads its moves and line offsets from the edited file. The rest of m_gcode_result,
|
||||
// including nozzle_group_result (consumed by the H2C print-dispatch nozzle mapping), is kept.
|
||||
if (m_fff_print->is_BBL_printer()) {
|
||||
run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config());
|
||||
if (run_post_process_scripts(m_temp_output_path, false, "File", m_temp_output_path, m_fff_print->full_print_config()))
|
||||
m_fff_print->reload_gcode_moves(m_gcode_result);
|
||||
}
|
||||
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": export gcode finished");
|
||||
|
||||
@@ -843,9 +843,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
|
||||
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
|
||||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
|
||||
|
||||
// gyroid_optimized only applies when the sparse infill pattern is gyroid;
|
||||
// The sparse infill density is the surface density of the adaptive TPMS infill; at 100% the infill is solid.
|
||||
bool have_tpms_infill = have_infill && config->option<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.
|
||||
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid);
|
||||
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_tpms_adaptive);
|
||||
|
||||
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
|
||||
if (have_infill) {
|
||||
|
||||
@@ -895,10 +895,16 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, f
|
||||
auto update_lines = [this](uint64_t start_id, uint64_t end_id) {
|
||||
std::vector<Line> ret;
|
||||
ret.reserve(end_id - start_id + 1);
|
||||
// Orca: m_lines_ends indexes into a memory mapping, so it must be clamped to the mapping. If the
|
||||
// file was modified behind our back (an in-place post-processing script that shrank it), an
|
||||
// unchecked read is an access violation, which the caller's try/catch cannot catch on Windows.
|
||||
const size_t file_size = m_file.size();
|
||||
for (uint64_t id = start_id; id <= end_id; ++id) {
|
||||
// read line from file
|
||||
const size_t start = id == 1 ? 0 : m_lines_ends[id - 2];
|
||||
const size_t original_len = m_lines_ends[id - 1] - start;
|
||||
// Keep one entry per id: render() indexes m_lines by (id - start_id).
|
||||
const size_t start = id == 1 ? 0 : std::min(m_lines_ends[id - 2], file_size);
|
||||
const size_t end = std::min(m_lines_ends[id - 1], file_size);
|
||||
const size_t original_len = end > start ? end - start : 0;
|
||||
// A character is four bytes at most, so 55 of them always fit in 220.
|
||||
const size_t len = std::min(original_len, (size_t) 55 * 4);
|
||||
std::string gline(m_file.data() + start, len);
|
||||
|
||||
@@ -7204,11 +7204,11 @@ void GUI_App::sync_preset(Preset* preset, bool force)
|
||||
|
||||
BOOST_LOG_TRIVIAL(trace) << "sync_preset: sync operation: " << preset->sync_info << " success! preset = " << preset->name;
|
||||
if (preset->type == Preset::Type::TYPE_FILAMENT) {
|
||||
preset_bundle->filaments.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
|
||||
preset_bundle->filaments.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id());
|
||||
} else if (preset->type == Preset::Type::TYPE_PRINT) {
|
||||
preset_bundle->prints.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
|
||||
preset_bundle->prints.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id());
|
||||
} else if (preset->type == Preset::Type::TYPE_PRINTER) {
|
||||
preset_bundle->printers.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time);
|
||||
preset_bundle->printers.set_sync_info_and_save(preset->name, setting_id, updated_info, update_time, m_agent->get_user_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7907,7 +7907,7 @@ void GUI_App::force_push_conflicting_preset(const std::string& setting_id)
|
||||
? OrcaCloudServiceAgent::generate_uuid_for_setting_id(preset.name, user_id)
|
||||
: preset.setting_id;
|
||||
if (preset_id == setting_id) {
|
||||
coll->set_sync_info_and_save(preset.name, setting_id, "update", 0);
|
||||
coll->set_sync_info_and_save(preset.name, setting_id, "update", 0, user_id);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,6 +396,8 @@ public:
|
||||
Slic3r::TaskManager* getTaskManager() { return m_task_manager; }
|
||||
HMSQuery* get_hms_query() { return hms_query; }
|
||||
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.
|
||||
void switch_printer_agent();
|
||||
|
||||
@@ -148,6 +148,9 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
|
||||
{"sparse_infill_density", "", 1},
|
||||
{"fill_multiline", "", 1},
|
||||
{"sparse_infill_pattern", "", 1},
|
||||
{"tpms_adaptive", "", 1},
|
||||
{"tpms_interior_density", "", 1},
|
||||
{"tpms_adaptive_gradient", "", 1},
|
||||
{"sparse_infill_smooth_factor", "", 1},
|
||||
{"lateral_lattice_angle_1", "", 1},
|
||||
{"lateral_lattice_angle_2", "", 1},
|
||||
|
||||
@@ -47,6 +47,9 @@ public:
|
||||
const std::string& get_vendor() 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_mesa() const;
|
||||
|
||||
+53
-12
@@ -15076,7 +15076,8 @@ bool Plater::priv::undo_redo_blocked_by_job()
|
||||
return false;
|
||||
notification_manager->push_notification(NotificationType::CustomNotification,
|
||||
NotificationManager::NotificationLevel::RegularNotificationLevel,
|
||||
_u8L("Cannot undo or redo while an operation is running. Stop it first."));
|
||||
_u8L("Cannot undo or redo while an operation is running. Stop the operation, or wait "
|
||||
"for it to finish and then retry."));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
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()
|
||||
{
|
||||
wxString path = p->get_export_file(FT_3MF);
|
||||
@@ -19325,6 +19358,23 @@ void Plater::export_core_3mf()
|
||||
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
|
||||
// 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).
|
||||
@@ -19374,19 +19424,10 @@ int Plater::export_published_3mf(const std::vector<std::string>& published_keys,
|
||||
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,
|
||||
// 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);
|
||||
}
|
||||
SvgArchivePathsRestorer svg_paths(model);
|
||||
|
||||
auto restore_temporary_state = [&]() {
|
||||
for (const auto& [path_in_3mf, previous_path] : previous_svg_paths)
|
||||
*path_in_3mf = previous_path;
|
||||
svg_paths.restore();
|
||||
|
||||
if (!had_model_info) {
|
||||
model.model_info = nullptr;
|
||||
|
||||
@@ -565,6 +565,9 @@ public:
|
||||
void export_gcode_3mf(bool export_all = false);
|
||||
void send_gcode_finish(wxString name);
|
||||
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
|
||||
// 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);
|
||||
|
||||
@@ -2961,6 +2961,9 @@ void TabPrint::build()
|
||||
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
|
||||
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
|
||||
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
|
||||
optgroup->append_single_option_line("tpms_adaptive", "strength_settings_patterns#adaptive-density");
|
||||
optgroup->append_single_option_line("tpms_interior_density", "strength_settings_patterns#interior-density");
|
||||
optgroup->append_single_option_line("tpms_adaptive_gradient", "strength_settings_patterns#adaptive-gradient");
|
||||
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
|
||||
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
|
||||
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
|
||||
|
||||
@@ -1129,6 +1129,19 @@ std::string OrcaCloudServiceAgent::request_setting_id(std::string name,
|
||||
if (http_code)
|
||||
*http_code = result.http_code;
|
||||
|
||||
// 409 duplicate_profile_uuid in the create path means the deterministic id we
|
||||
// just generated already exists in this account: the earlier create succeeded.
|
||||
// Adopt it instead of failing, so sync_preset persists the id and stops retrying.
|
||||
if (result.http_code == 409 && result.conflict_code == -2
|
||||
&& !result.server_version.id.empty() && result.server_version.id == new_id) {
|
||||
if (values_map && result.server_version.updated_time != 0)
|
||||
(*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.server_version.updated_time);
|
||||
if (http_code)
|
||||
*http_code = 200;
|
||||
BOOST_LOG_TRIVIAL(info) << "OrcaCloudServiceAgent: request_setting_id adopted existing profile id " << new_id << " (409 duplicate_profile_uuid)";
|
||||
return new_id;
|
||||
}
|
||||
|
||||
if (result.success) {
|
||||
if (values_map && result.new_updated_time != 0) {
|
||||
(*values_map)[IOT_JSON_KEY_UPDATED_TIME] = std::to_string(result.new_updated_time);
|
||||
@@ -1394,6 +1407,7 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
|
||||
SyncPushResult result;
|
||||
result.success = false;
|
||||
result.http_code = 0;
|
||||
result.conflict_code = 0;
|
||||
result.server_deleted = false;
|
||||
|
||||
nlohmann::json body;
|
||||
@@ -1429,20 +1443,30 @@ SyncPushResult OrcaCloudServiceAgent::sync_push(const std::string& profile_id,
|
||||
err_body = json;
|
||||
if (json.is_null()) {
|
||||
result.server_deleted = true;
|
||||
} else {
|
||||
auto& profile_data = json["server_profile"];
|
||||
result.server_version.id = profile_data.value("id", "");
|
||||
result.server_version.name = profile_data.value("name", "");
|
||||
result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
|
||||
} else if (json.is_object()) {
|
||||
result.conflict_code = json.value("code", 0);
|
||||
if (json.contains("server_profile") && !json["server_profile"].is_null()) {
|
||||
auto& profile_data = json["server_profile"];
|
||||
result.server_version.id = profile_data.value("id", "");
|
||||
result.server_version.name = profile_data.value("name", "");
|
||||
result.server_version.updated_time = profile_data.value(ORCA_JSON_KEY_UPDATE_TIME, 0);
|
||||
}
|
||||
}
|
||||
} catch (...) {}
|
||||
// Surface the conflict via the http-error callback with the local preset name injected.
|
||||
// The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
|
||||
// but the GUI needs it to regenerate the deterministic setting_id for a force push.
|
||||
if (!err_body.is_object())
|
||||
err_body = nlohmann::json::object();
|
||||
err_body["name"] = name;
|
||||
invoke_http_error_callback(409, err_body.dump());
|
||||
// Create-path duplicate_profile_uuid (-2) is an idempotent success: the deterministic id
|
||||
// already exists, so the caller adopts the returned id. Skip the conflict notification,
|
||||
// otherwise every already-imported preset would raise a Pull/Force-push prompt on each launch.
|
||||
const bool is_create = original_updated_time.empty();
|
||||
const bool auto_resolved_duplicate = (is_create && result.conflict_code == -2);
|
||||
if (!auto_resolved_duplicate) {
|
||||
// Surface the conflict via the http-error callback with the local preset name injected.
|
||||
// The raw server body omits the name for tombstone (-3) conflicts (server_profile is null),
|
||||
// but the GUI needs it to regenerate the deterministic setting_id for a force push.
|
||||
if (!err_body.is_object())
|
||||
err_body = nlohmann::json::object();
|
||||
err_body["name"] = name;
|
||||
invoke_http_error_callback(409, err_body.dump());
|
||||
}
|
||||
result.error_message = response;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -94,6 +94,7 @@ struct SyncPullResponse {
|
||||
struct SyncPushResult {
|
||||
bool success;
|
||||
int http_code;
|
||||
int conflict_code;
|
||||
long long new_updated_time;
|
||||
ProfileUpsert server_version;
|
||||
bool server_deleted;
|
||||
|
||||
@@ -1,16 +1,36 @@
|
||||
#include "PluginHostBindings.hpp"
|
||||
#include <pybind11/cast.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/pytypes.h>
|
||||
|
||||
#include <libslic3r/Model.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_App.hpp>
|
||||
#include <slic3r/GUI/OpenGLManager.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 <stdexcept>
|
||||
#include <wx/app.h>
|
||||
#include <wx/string.h>
|
||||
#include <wx/thread.h>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
@@ -41,6 +61,94 @@ PresetBundle* current_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
|
||||
|
||||
// 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("is_project_dirty", &GUI::Plater::is_project_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", ¤t_plater, py::return_value_policy::reference);
|
||||
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");
|
||||
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
|
||||
|
||||
@@ -400,6 +400,17 @@ void PluginPages::relayout()
|
||||
}
|
||||
}
|
||||
|
||||
void PluginPages::select_page(const PluginCapabilityId& id)
|
||||
{
|
||||
if (m_parent == nullptr || m_pages.find(id) == m_pages.end())
|
||||
return;
|
||||
|
||||
// Swap the page into the visible slot first when it lives behind the overflow menu.
|
||||
m_swapped_in_id = id;
|
||||
relayout();
|
||||
m_parent->SelectPageByName(page_tab_id(id));
|
||||
}
|
||||
|
||||
void PluginPages::show_overflow_menu()
|
||||
{
|
||||
const int visible_slots = std::max(1, m_visible_page_count);
|
||||
|
||||
@@ -66,6 +66,8 @@ public:
|
||||
|
||||
void relayout();
|
||||
|
||||
void select_page(const PluginCapabilityId& id);
|
||||
|
||||
private:
|
||||
std::shared_ptr<PagesPluginCapability> get_pages_cap(const PluginCapabilityId& id, bool is_enabled) const;
|
||||
bool create_page(const PluginCapabilityId& id);
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "libslic3r/Fill/Fill.hpp"
|
||||
#include "libslic3r/Fill/FillAdaptive.hpp"
|
||||
#include "libslic3r/Fill/FillGyroid.hpp"
|
||||
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
|
||||
#include "libslic3r/Flow.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
#include "libslic3r/IntersectionPoints.hpp"
|
||||
@@ -2021,3 +2022,334 @@ TEST_CASE("Adaptive infill of a modifier leaves the density of the other regions
|
||||
CHECK(unmatched_between_prints(print, print_sparse, erInternalInfill, {rect(3, 3, 27, 27), rect(43, 3, 52, 27)}) < 0.02);
|
||||
CHECK(unmatched_between_prints(print, print_dense, erInternalInfill, {rect(58, 3, 67, 27)}) < 0.02);
|
||||
}
|
||||
|
||||
// Length of the sparse infill of the first object over the layers with print_z in [z_min, z_max], inside clip.
|
||||
static double sparse_infill_length(const Print &print, coordf_t z_min, coordf_t z_max, const Polygons &clip)
|
||||
{
|
||||
Polylines polylines;
|
||||
for (const Layer *layer : print.objects().front()->layers())
|
||||
if (layer->print_z >= z_min && layer->print_z <= z_max)
|
||||
for (const LayerRegion *region : layer->regions())
|
||||
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
|
||||
if (entity->role() == erInternalInfill)
|
||||
entity->collect_polylines(polylines);
|
||||
return unscale<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);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include "libslic3r/Config.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
@@ -221,3 +222,62 @@ TEST_CASE("Line ends of the exported G-code mark every newline in the file", "[G
|
||||
INFO("first difference at line " << difference.first - result.lines_ends.begin() + 1);
|
||||
CHECK(difference.first == result.lines_ends.end());
|
||||
}
|
||||
|
||||
TEST_CASE("Reloaded moves name their lines in G-code a script rewrote in place", "[GCodeProcessor]")
|
||||
{
|
||||
Print print;
|
||||
Model model;
|
||||
Test::init_print({ Test::cube(20) }, print, model);
|
||||
GCodeProcessorResult result;
|
||||
const std::string gcode = Test::gcode(print, &result);
|
||||
const auto exported_moves = result.moves;
|
||||
|
||||
// A script that prepends one comment and, writing in text mode on Windows, turns every LF into CRLF.
|
||||
const std::string prepended = ";EDITED\r\n";
|
||||
std::string edited = prepended;
|
||||
for (const char c : gcode) {
|
||||
if (c == '\n')
|
||||
edited += '\r';
|
||||
edited += c;
|
||||
}
|
||||
ScopedTemporaryFile temp(".gcode");
|
||||
save_string_file(temp.path(), edited);
|
||||
result.filename = temp.string();
|
||||
print.reload_gcode_moves(&result);
|
||||
|
||||
std::vector<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);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ get_filename_component(_TEST_NAME ${CMAKE_CURRENT_LIST_DIR} NAME)
|
||||
add_executable(${_TEST_NAME}_tests
|
||||
${_TEST_NAME}_tests.cpp
|
||||
test_3mf.cpp
|
||||
test_amf.cpp
|
||||
# Round-trip seam metadata and active/dormant volume settings in both formats.
|
||||
test_precise_seam_3mf.cpp
|
||||
# Pure perimeter extraction is independent of Print/Layer fixtures.
|
||||
@@ -34,6 +35,7 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_fill_corner_smoothing.cpp
|
||||
test_filament_mixer.cpp
|
||||
test_fill_plane_path.cpp
|
||||
test_fill_tpms_adaptive.cpp
|
||||
test_geometry.cpp
|
||||
test_multimaterial_segmentation.cpp
|
||||
test_placeholder_parser.cpp
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
[¢er](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));
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
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);
|
||||
try {
|
||||
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]")
|
||||
{
|
||||
py::object host = import_orca_module().attr("host");
|
||||
|
||||
Reference in New Issue
Block a user