Compare commits

..
Author SHA1 Message Date
SoftFever f6e8821b13 Merge branch 'main' into perf/slicing-optimizations 2026-10-10 01:09:46 +08:00
Misterff1 b5ef24e7ff Fix regression: Arc Fitting setting for BBL P2S (#16319)
Disable Arc Fitting for BBL P2S
2026-10-10 00:32:46 +08:00
Ian Chua 8585eae816 fix: hide symbols of the bundled static openssl (#16317)
* fix: hide symbols of the bundled static openssl

* fix: hide the bundled static OpenSSL symbols on Linux

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

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

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

* Add tests for rebuilding the G-code line offsets

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

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

---------

Co-authored-by: SoftFever <103989404+SoftFever@users.noreply.github.com>
Co-authored-by: SoftFever <softfeverever@gmail.com>
2026-10-09 23:08:43 +08:00
SoftFever 076d88f34e Merge branch 'main' into perf/slicing-optimizations 2026-10-09 22:19:27 +08:00
SoftFever 7f554fde04 Restore the internal bridge expansion from #16056 lost in the main merge 2026-10-09 22:11:47 +08:00
SoftFever d704ee5538 Merge branch 'main' into perf/slicing-optimizations
# Conflicts:
#	src/libslic3r/PerimeterGenerator.cpp
#	src/libslic3r/PrintObject.cpp
#	src/libslic3r/Support/TreeSupport.cpp
2026-10-09 19:48:26 +08:00
Rodrigo Faselli 7de1dc1548 Merge branch 'main' into perf/slicing-optimizations 2026-10-04 13:26:02 -03:00
Rodrigo Faselli b6140110f2 Merge branch 'main' into perf/slicing-optimizations 2026-10-03 10:57:31 -03:00
Ian Bassi 7b0e2f3ce5 Keep the G-code identical to main on Clipper2
Since main moved to Clipper2, parts of this branch no longer gave the same
G-code as main:

- bridge_over_infill dropped expand(limiting_area, 0.3 * flow.spacing()) as
  a no-op. The offset is below one unit, but Clipper2 still unites its
  result, which splits and merges touching polygons and so changes the
  anchor lines. Running it on the polygons next to the bridge gives main's
  anchors without the whole-layer pass.
- tsp_remove_crossings stopped at the first repeated ordering, where main
  runs on to its pn * pn cap. The loop is periodic from that point, so it
  now takes only the steps to the ordering main stops on.
- With a single tile, the tiled booleans now make the plain call instead of
  cutting the clip to the tile first.

The tiled boolean test compared rings exactly. With the safety offset a tile
unites only the clip polygons near it, and Clipper2 can then round a
crossing 1 unit differently, so that case allows 1 unit.

Comments that named ClipperLib now say Clipper, and
docs/HLSD/polygon-clipping.md describes the tiled booleans.

G-code of the five handy models in four configurations and of a baked
texture relief is byte-identical to main. Colour-painted models still
differ: segmenting each island on its own splits a colour's region into
different pieces than one diagram over the layer, which on one model also
changes the first layer's tool order.
2026-10-02 20:26:59 -03:00
ExPikaPaka d80c69341c Say what the code does, not what it replaced
The timings and the runs that never finished belong in the commit
messages, where they can be read against the change; a reader of the
code cannot check them. Kept the cost that still explains the design.

MultiPoint also spells out the consequence: a moved-from Polygon or
Polyline is now really empty where it used to silently keep its points.

The two wall spacing comments the parallel loop reindented are plain
ASCII now, so the whole file is.
2026-10-02 18:13:46 -03:00
ExPikaPaka 4a63a7d916 Compare the tiled booleans polygon by polygon
Area alone would pass on a result whose pieces were merged across tiles
or which kept the cut edges of the clip. The rings are compared after
rotating each to its lowest point and sorting, so only the ordering is
free. The fixture now also asserts it really is split into more than one
tile, which is the path being tested.
2026-10-02 18:13:46 -03:00
ExPikaPaka de1dfd0611 Hand the island's walls over instead of copying them
append(const ExtrusionEntity &) clones; the collection each island
produced was deep-copied into the layer's loops and then thrown away.
The no-overlap areas are moved as well.
2026-10-02 18:13:46 -03:00
ExPikaPaka 11a5971cef Include what the new code uses
The Windows build stopped on test_kdtree.cpp: it calls std::iota without
including <numeric>, which libstdc++ happens to pull in anyway. Added
there, and the same for <limits> and <algorithm>/<cmath> where the
recent changes rely on them being included by something else.
2026-10-02 18:13:45 -03:00
ExPikaPaka 82f37ddb3a Fix the Windows build: near and far are macros there
bridge_over_infill's helper for splitting polygons by proximity named
its locals near and far. The Windows headers define both as macros that
expand to nothing, so "Polygons near;" declared nothing and the uses of
it did not compile. Renamed; no behaviour change.
2026-10-02 18:13:37 -03:00
ExPikaPaka fb03d1a1cb Visit seam candidates as the search finds them
Collecting every candidate within the radius into a vector cost more
than the search itself. Same order, so the seams are unchanged;
align_seam_points ~19.6 s at 0.1 mm / 2000k, was ~21.
2026-10-02 18:13:37 -03:00
ExPikaPaka 2b4bdead73 Run a layer's regions in parallel where they are independent
detect_surfaces_type, process_external_surfaces and the vertical shells
each waited on their own heaviest layer in turn. The LOTR map plate
slices in ~10.5 min at 0.1 mm / 2000k, was ~11.5; ~87 s at normal
settings, was ~97.
2026-10-02 18:13:37 -03:00
ExPikaPaka b1f0d6c6f6 Move polygons instead of copying them on move
MultiPoint had no rvalue constructor, so the derived move constructors
bound to the const reference and copied; append reserved exactly, so
collecting pieces one by one was quadratic. Colour segmentation ~3 s at
0.1 mm / 2000k, was ~40, and ordinary prints gain too.
2026-10-02 18:13:37 -03:00
ExPikaPaka 14751a8b06 Project painted faces onto the shell layers per tile
Only the slices within the deepest shell offset decide the result, so
the work is done per tile of the face. Top and bottom segmentation
~130 s at 0.1 mm / 2000k, was ~180.
2026-10-02 18:13:36 -03:00
ExPikaPaka 9a86d79038 Tile the booleans on layers of many pieces
ClipperLib slows down with the number of edges on a scan line, and a
layer cut through a fine relief has tens of thousands of pieces.
detect_surfaces_type ~50 s at 0.1 mm / 2000k, was ~145.
2026-10-02 18:13:05 -03:00
ExPikaPaka d905f1a39b Merge colour and top/bottom regions per island
The merge took anything from 3 to 38 minutes at 0.1 mm / 2000k, now
~2.5. Every region is grouped with the islands it overlaps, so the
result is the same.
2026-10-02 18:12:41 -03:00
ExPikaPaka 84ec518f26 Slice fine texture relief without stalling
A colour texture baked at 0.1 mm / 2000k made the top layers thousands
of islands and slicing never finished. Colour segmentation runs per
island, the merge subtracts piece by piece, the support check tests only
nearby islands, and the travel ordering finds crossings through a grid.
2026-10-02 18:12:41 -03:00
ExPikaPaka 1fc153308f Generate walls and split solid infill in parallel
Same output, ~2.2 min for the LOTR map plate, was ~2.6.
2026-10-02 18:12:41 -03:00
ExPikaPaka 3167c3665a Run colour segmentation and vertical shells in parallel
Same output, ~2.6 min for the LOTR map plate, was ~2.9.
2026-10-02 18:12:40 -03:00
ExPikaPaka 6d34d83e78 Faster slicing of colour-painted layers
A layer split into ~1000 colour fragments (a colour texture baked over a
large top face) made several per-fragment loops redo whole-layer ClipperLib
work, so slicing took ~33 min; it now takes ~3 min with the same output.

- make_fills: clip the layer's no-overlap area to each expolygon's box
  before intersecting
- discover_vertical_shells: small-piece filter compares only against the
  nearby part of the layer
- bridge_over_infill: whole-layer union/diff/intersections restricted to the
  candidate's neighbourhood; fill boundary expanded once per spacing; anchor
  tree built only from lines crossing the scan range; bbox pre-check in the
  collision test; limiting outline taken directly instead of through
  expand(..., 0.3 * flow.spacing()), which offsets by 0.135 scaled units
  (flow.spacing() is in mm) and only cost a whole-layer pass per candidate
2026-10-02 18:11:17 -03:00
86 changed files with 4286 additions and 2032 deletions
+29
View File
@@ -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 ()
+24
View File
@@ -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 ()
+221
View File
@@ -0,0 +1,221 @@
# Adaptive TPMS infill — High Level Design
## Purpose and scope
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top
and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it.
The design has two parts: a field built once per object, and a pattern made
from it, warped around the center of each lobe of a body so that its cell size
follows the field, or, in the modes following the distance to the surface,
split into shells or blended between densities.
## Radial field
`TpmsRadialField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the
field depends on the geometry only, the densities are applied per region in the
fill. An object thinner than the grid cells has no body in the field; no field
is kept then, and the infill falls back to the regular pattern. In the modes
following the distance to the surface, the field also gives the depth of every
point (see below).
A regular 3D grid of cubic cells is rasterized from the `lslices` of the layers,
so the field follows what is printed: negative volumes, the union of
overlapping parts and holes are taken into account, and the mesh does not need
to be closed. A padding node around the grid is always outside. The grid is
capped at about a million nodes, with cells no smaller than 0.5 mm.
- Bodies are the connected inside nodes. Each is graded on its own, so separate
parts of one object each get their own sparse center.
- A body is split into lobes around the local maxima of the depth, by an exact
Euclidean distance transform (Felzenszwalb and Huttenlocher, one pass per
axis). Two maxima are in separate lobes when the depth along the segment
between them drops below 0.8 of the shallower one, like at the neck between
two united spheres; maxima shallower than 0.3 of the deepest one are ignored.
A maximum joins the first lobe whose first maximum it sees without a neck.
The lobes are made one at a time, the remaining maxima tested against the
first one in parallel, as a plate has a whole plane of them.
Where the depth ties along a line or a plane, as in a tall box, the lobe's
center is the node nearest to the middle of the tied nodes, so the center is
in the middle of the height and not a column.
- A point belongs to the lobe it is nearest to relative to their depths, so the
side between two lobes is nearer to the smaller one. Near that side, within a
tenth of that relative distance, the patterns of the lobes morph into each
other, so the lines stay continuous. Every lobe in that range takes part, up
to four, so the morph is also continuous where three or four lobes meet.
- The reach of a lobe is the distance from its center to the first exit along
24 x 48 latitude-longitude directions, smoothed twice over neighbouring
directions in log space. Towards a neighbouring lobe it stops at twice the
distance to the side between them, so that side is graded half way, as deep
as a neck is, rather than as sparse as the center or as dense as the surface.
Only the lobes whose centers are near enough to be nearer at the current
distance are compared along a ray, so many lobes, as in a perforated plate,
stay cheap.
The radial coordinate of a point is its distance to the center over the reach
in its direction. Behind a gap, as across the hole
of a ring, the radial coordinate is above 1 and the infill keeps the surface
density.
- Every outside node belongs to its nearest body, so points near a surface find
their body without a search. With a single body, all nodes belong to it.
In the 2D modes, every plane of nodes normal to the axis is a field of its own:
the distance transform skips the axis, bodies, lobes and the nearest body are
found within the plane, and the reach is sampled on a circle of 48 directions.
A point is looked up in the two planes around it, the weights of their lobes
interpolated along the axis, so the grading does not step between planes; a
plane without a body uses the nearest one that has one. The planes are a cell
apart, not a layer: where the sections change abruptly, as at a step, the
patterns of the two planes morph into each other over that cell.
A distance to the nearest surface would be the obvious field, but no smooth map
follows it. By the divergence theorem, the mean scale of a map over a body is
fixed by its values on the surface: a map that keeps the full density along the
whole surface, as the distance would ask under the top and bottom, has the mean
density of the uniform infill, the sparser core being paid for by lines crowding
along the walls. The layers of a plate at different depths would also need
different line spacings in the same directions, which no continuous map allows
without shearing across the plate. Following the distance needs changes of the
topology of the lattice (see below). The radial coordinate instead grades what a
single map can: towards one point.
## Warped pattern
The pattern is evaluated on warped coordinates:
TPMS(f_surface * m(t) * (p - center))
where `m` scales the pattern around the center of the lobe: its frequency is
`m + t * m'` along the ray and `m` across it. `m(t)` is the mean of the target
scale over the ball of radius `t`, `3 / t^3 * integral of s^2 * target(s) ds`, so
the mean of the three, and with it the density, follows the gradient. The cells
are round at the center; near the surface they are flattened, with the lines
running parallel to it. Beyond the surface the target is the surface scale, so
the warp extends continuously outside.
In the 2D modes only the coordinates within the plane are warped, and `m(t)` is
the mean over the disc, `2 / t^2 * integral of s * target(s) ds`. Along the axis
the pattern keeps the interior frequency: scaling it with `m` would shear the
pattern by the distance along the axis times the gradient of `m`, without bound
on a long object. The cells are round at the center and stretched along the
axis near the surface. With Normal Z the layers are graded exactly, since
the lines of a layer follow its in-plane frequencies; normal to X or Y, the
layers near the sides are as dense as the larger of the two frequencies in the
layer, which is the surface one.
Evaluating a TPMS at a frequency that varies with the position without such a
map distorts it wherever the frequency changes, because the phase also changes
with the gradient of the frequency times the distance from the origin. Fitting a
smooth map to a varying isotropic scale in the least-squares sense (a Poisson
problem per axis) cannot grade strongly: its divergence is the target scale plus
a harmonic function pinned by the surface, which keeps the scale in the core
near two thirds of the surface one. Following a distance exactly needs the
lattice to change its topology, by blending lattices of different densities or
filling shells of equal distance with them, as the modes following the distance
to the surface do.
The target scale at depth `d = 1 - t`, with `S` the surface and `I` the interior
frequency, both from each pattern's own density calibration:
| Gradient | Scale |
|-------------|------------------------|
| Linear | `1 + (I / S - 1) * d` |
| Quadratic | `1 + (I / S - 1) * d^2`|
| Exponential | `(I / S)^d` |
With a denser surface, quadratic keeps the surface density deepest and
exponential drops fastest. A denser interior works the same way.
The zero level is extracted with marching squares like the regular TPMS-FK, on
a sampling grid fixed in the fill frame like the optimized Gyroid, so that every
region of a layer connects its lines the same way at the saddles of the pattern.
Loops narrower than two lines (shorter than `2 * PI * spacing`) are dropped, as
they would print as blobs. The fill works in a frame rotated by the infill
angle, so the radial field is looked up at the point rotated back into the
object frame, and the center rotated into the fill frame. Both use the middle of
the layer.
## Modes following the distance to the surface
`distance_warp`, `smooth_blend` and `stepped_shells` grade by the distance to
the nearest surface, including the top and bottom, as concentric shells do. The
depth of a point is that distance over the distance of the deepest point of its
body, from 0 at the surface to 1; the field keeps it for every node and
interpolates it between them. A tall box so keeps its whole axis as sparse as
its center, and a plate is graded through its thickness.
No single smooth pattern follows that depth without distortion (see above), so
the three modes trade differently:
- Distance warp keeps the lobes and the warp of Lobes, but its radial profile
comes from the depth. For every direction of a lobe, the mean depth over the
ball along the ray is sampled at 33 radii up to the reach, then smoothed over
the neighbouring directions like the reach. The radial coordinate is the one
of the linear profile with the same mean depth, `t = 4/3 * (1 - mean depth)`,
so with a linear gradient the mean cell size follows the depth exactly, and
with the others approximately. In a sphere or a cube, where the depth falls
linearly along every ray, it is Lobes. Elsewhere the profile changes with the
direction, and the warp shears where neighbouring directions differ, as in
plates and long bodies; right under the top of a long body the cells are
sparser within the layer, the warp moving their density into the height.
The profiles are smoothed over the directions like the reach, as sharper
ones shear the pattern across the layer, which adds lines. A long body is so
graded partly along its length, between Lobes and the distance.
- Smooth blend evaluates the regular patterns of the two levels around the
target of every point and blends them by a smoothstep over the whole gap
between the levels, here at most 2.5 times apart. The density follows the
depth without steps, but where two lattices blend, part of their lines run
along the blend, so fewer levels print fewer extra lines. From 25% to 5%,
three levels print about 0.45 of the uniform infill in a deep core whose
levels alone would print 0.3; levels 1.5 times apart print 0.6 to 0.7, and a
single blend from the surface to the interior 0.6.
- Stepped shells split each region of a layer into shells and fill every shell
with the regular pattern at its density. The densities are levels from the
surface to the interior density at most 1.5 times apart, five from 25% to 5%,
and a point takes the level nearest to its target on that geometric scale.
The shells are traced by marching squares of the continuous level over the
layer on a 0.5 mm grid fixed in the object, so every region of a layer gets
the same shells, and clipped to the region. Each shell is shrunk by half a
line, like a filled region, and its regular filler connects its lines along
that boundary, so the connections of two neighbouring shells lie side by side
instead of on top of each other. The pattern is never
distorted, but its lines end at every shell, and thin parts get thin shells.
The connections add lines: in the core of a 60 mm cube, about a third more
than the target.
Stepped shells and Smooth blend need neither lobes nor reaches, which are not
built for them.
## Constraints
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large.
- At a sparse infill density of 100% the sparse infill is turned into solid
infill, so there is nothing to grade: the options are hidden and no field is
built.
- The adaptive options invalidate `posPrepareInfill`, which rebuilds the field
and the anchoring infill.
- An elongated body without a neck has one center, so its far ends are graded
as the outer part of the body, and the warp shears where the reach changes
quickly with the direction. A concave body, like an L, may be split into lobes
where its maxima cannot see each other in a straight line.
- Across a ray, the scale is the mean of the gradient from the center, so the
layers right under the top and above the bottom are sparser than the surface
density in their middle, and a plate is graded from its middle outwards rather
than through its thickness.
+17
View File
@@ -62,6 +62,23 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
clip do not leave slivers.
- Open polylines are clipped with the non-zero rule and keep their direction.
### Tiled booleans
The sweep slows down with the number of edges crossing a scan line, so a layer
cut into thousands of pieces makes every whole-layer boolean expensive.
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
non-overlapping `ExPolygons`, group them into tiles with
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
The result covers the same area as the plain call. Without the safety offset
the rings are the same. With it, each tile unites only the clip polygons near
it, so a clip edge that the whole-layer union splits where it crosses a distant
clip polygon stays whole, and a crossing with the subject can round 1 unit
differently. The tiles' results are concatenated in tile order, so the order of
the output `ExPolygons` differs from the plain call.
### Offsets
- Before offsetting, input vertices closer than
@@ -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"
@@ -29,11 +29,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
// The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform vec3 filament_rgb[16];
uniform int filament_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -208,16 +210,31 @@ int nearest_palette_entry(vec3 rgb)
best = i;
}
}
// The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// where the bake prints a single filament. Compared on the distances rather than their squares, so
// the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so
// it is only worth taking when it beats the nearest single filament by a visible step. Without it
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries
// against 30 mixes - while the bake picked a single filament for most of them, so the preview
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
best = best_pure;
return best;
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -337,11 +354,16 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour -
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture2D(color_tex, color_uv).rgb)];
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture2D(color_tex, color_uv).rgb));
gl_FragColor = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
@@ -88,11 +88,13 @@ uniform vec3 palette_lab[64];
uniform vec3 palette_rgb[64];
uniform int palette_count;
uniform bool pure_only; // match against single filaments only (flat-colour image)
// The entry's two filaments, equal for a single filament - only so a mix can be told apart. An entry's
// palette_rgb is already the colour it prints in (for a mix, its mixed filament slot's).
// How each entry prints. Every entry names a single filament: a mix is given its own mixed filament
// slot, whose components the slicer alternates per print layer, so the fragment just looks that slot's
// colour up.
uniform int palette_a[64];
uniform int palette_b[64];
uniform float prefer_pure_de; // PREFER_PURE_DE: how much better than a single filament a mix must be
uniform vec3 filament_rgb[16];
uniform int filament_count;
uniform sampler2D color_tex; // the layer's colour image, sampled at the same uv as the height
uniform bool has_color_tex;
uniform bool volume_mirrored;
@@ -274,16 +276,31 @@ int nearest_palette_entry(vec3 rgb)
best = i;
}
}
// The same bias make_palette_quantizer() applies: a mix is an interleave, so it is only worth taking
// when it beats the nearest single filament by a visible step - otherwise the preview shows mixes
// where the bake prints a single filament. Compared on the distances rather than their squares, so
// the margin means the same thing as it does on the CPU (up to CIE76 against CIEDE2000, the
// approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < prefer_pure_de)
// The same bias make_palette_quantizer() applies (PREFER_PURE_DE = 10): a mix is an interleave, so
// it is only worth taking when it beats the nearest single filament by a visible step. Without it
// this picked a mix for almost every fragment - with four filaments the palette is 4 pure entries
// against 30 mixes - while the bake picked a single filament for most of them, so the preview
// interleaved the whole wall where the bake interleaves only patches. Compared on the distances
// rather than their squares, so the threshold means the same thing as it does on the CPU (up to
// CIE76 against CIEDE2000, the approximation already noted above).
if (best_pure >= 0 && palette_a[best] != palette_b[best] && sqrt(bd_pure) - sqrt(bd) < 10.0)
best = best_pure;
return best;
}
// One 2x2 Bayer cell, {0, 2; 3, 1}, for x and y in {0, 1}.
// The colour the printer lays down at world point `pos` for palette entry `index`. Every entry names a
// single filament: a mix is given its own mixed filament slot, whose components the slicer alternates
// per print layer, so there is nothing left to interleave here.
vec3 printed_color(int index)
{
int a = palette_a[index];
if (a < 0 || a >= filament_count)
return palette_rgb[index]; // no filament to resolve to: the entry's own colour
return filament_rgb[a];
}
void main()
{
if (any(lessThan(clipping_planes_dots, ZERO)))
@@ -425,11 +442,16 @@ void main()
NdotL = max(dot(eye_normal, LIGHT_FRONT_DIR), 0.0);
intensity.x += NdotL * LIGHT_FRONT_DIFFUSE;
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour.
// Diffuse albedo: the image's colour at this fragment, snapped to the nearest printable colour -
// and, where that is a mix, the filament the interleave puts here, so the pattern that prints shows.
// Only the albedo - the specular term (intensity.y) stays white - so a coloured fragment reads as
// the same material under the same light, and the relief this preview exists to show is unaffected.
vec3 albedo = uniform_color.rgb;
if (palette_count > 0 && has_color_tex && have_uv && weight > 0.0)
albedo = palette_rgb[nearest_palette_entry(texture(color_tex, color_uv).rgb)];
// tex_pos, not world_pos: the bake resolves the interleave in the bake frame (world
// orientation and scale about the volume's origin, see texture_displacement_bake_frame()), so
// measuring z from the bed instead shifted the band phase by the volume origin's height - a
// different filament in the same place than the bake produces.
albedo = printed_color(nearest_palette_entry(texture(color_tex, color_uv).rgb));
out_color = vec4(vec3(intensity.y) + albedo * intensity.x, uniform_color.a);
}
+2
View File
@@ -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
+68
View File
@@ -9,6 +9,8 @@
#include <numeric>
#include <unordered_map>
#include <tbb/parallel_for.h>
#include "ClipperUtils.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
@@ -800,6 +802,72 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); }
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset)
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); }
namespace ClipperUtils {
std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile)
{
BoundingBox extent;
std::vector<BoundingBox> bboxes;
bboxes.reserve(expolygons.size());
for (const ExPolygon &expoly : expolygons) {
bboxes.emplace_back(get_extents(expoly));
extent.merge(bboxes.back());
}
if (! extent.defined)
return {};
const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max<size_t>(per_tile, 1)))), 1, 32);
const Point size = extent.size();
const coord_t tile_w = std::max<coord_t>(1, size.x() / tiles + 1), tile_h = std::max<coord_t>(1, size.y() / tiles + 1);
std::vector<ExPolygonsTile> out(size_t(tiles * tiles));
for (size_t i = 0; i < expolygons.size(); ++ i) {
const Point c = bboxes[i].center();
ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles +
std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))];
tile.members.emplace_back(i);
tile.bbox.merge(bboxes[i]);
}
out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end());
return out;
}
}
static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{
// A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box.
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(subject, 32);
// One tile is the plain call: cutting the clip would only cost time.
if (tiles.size() <= 1)
return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset);
std::vector<BoundingBox> clip_bboxes;
clip_bboxes.reserve(clip.size());
for (const Polygon &polygon : clip)
clip_bboxes.emplace_back(get_extents(polygon));
std::vector<Slic3r::ExPolygons> out_tiles(tiles.size());
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
Slic3r::ExPolygons local_subject;
local_subject.reserve(tile.members.size());
for (size_t i : tile.members)
local_subject.emplace_back(subject[i]);
// Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset.
const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON);
Polygons local_clip;
for (size_t i = 0; i < clip.size(); ++i)
if (clip_bboxes[i].overlap(bbox))
if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty())
local_clip.emplace_back(std::move(clipped));
out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset);
});
Slic3r::ExPolygons out;
for (Slic3r::ExPolygons &out_tile : out_tiles)
append(out, std::move(out_tile));
return out;
}
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); }
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); }
// May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative).
Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); }
+15
View File
@@ -5,6 +5,7 @@
#include "Polyline.hpp"
#include "Line.hpp"
#include "libslic3r.h"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Polygon.hpp"
#include "Surface.hpp"
@@ -332,6 +333,15 @@ namespace ClipperUtils {
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
// Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a
// layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box.
struct ExPolygonsTile
{
BoundingBox bbox;
std::vector<size_t> members;
};
[[nodiscard]] std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile);
}
// offset Polygons
@@ -527,6 +537,11 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
// diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in
// parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a
// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line.
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
-54
View File
@@ -18,7 +18,6 @@
#include <utility>
#include "ColorDecomposeRecipe.hpp"
#include "Config.hpp"
#include "FilamentMixerModel.hpp"
#include "LocalesUtils.hpp"
@@ -428,59 +427,6 @@ std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_componen
return ratios;
}
std::string format_mixed_components(const std::vector<unsigned int> &components)
{
std::string out;
for (size_t i = 0; i < components.size(); ++i) {
if (i > 0)
out += ",";
out += std::to_string(components[i]);
}
return out;
}
std::string format_mixed_ratios(const std::vector<int> &weights)
{
int sum = std::accumulate(weights.begin(), weights.end(), 0);
if (sum <= 0)
sum = 100;
CNumericLocalesSetter c_locale_setter;
std::string out;
for (size_t i = 0; i < weights.size(); ++i) {
if (i > 0)
out += ",";
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", float(weights[i]) / float(sum));
out += buf;
}
return out;
}
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights)
{
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
return -1;
// Created lazily with the first mixed slot, so an older project may not have it at all.
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
const std::string comp_str = format_mixed_components(components);
const std::string ratio_str = format_mixed_ratios(weights);
for (size_t i = 0; i < is_mixed->values.size(); ++i) {
if (!is_mixed->values[i] || i >= comps->values.size() || i >= ratios->values.size())
continue;
if (gradient != nullptr && i < gradient->values.size() && gradient->values[i])
continue;
if (comps->values[i] == comp_str && ratios->values[i] == ratio_str)
return int(i);
}
return -1;
}
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed)
{
for (unsigned char v : is_mixed)
-18
View File
@@ -11,8 +11,6 @@
namespace Slic3r {
class ConfigBase;
// Photoshop-style gradient curve control point in [0,1] x [0,1].
// (x, y) is the anchor position; (m_in, m_out) are optional cubic Hermite tangent
// overrides. NaN means "use the PCHIP-computed default", which is the case for plain
@@ -96,22 +94,6 @@ std::vector<unsigned int> parse_mixed_components(const std::string &str);
// Normalizes so the sum equals 1.0.
std::vector<double> parse_mixed_ratios(const std::string &str, size_t n_components);
// The text a mixed slot stores in filament_mixed_components, e.g. {1, 3} → "1,3".
std::string format_mixed_components(const std::vector<unsigned int> &components);
// The text a mixed slot stores in filament_mixed_sublayer_ratios: the weights normalised to sum to 1,
// four decimals, e.g. {1, 2} → "0.3333,0.6667". A non-positive sum is read as 100.
std::string format_mixed_ratios(const std::vector<int> &weights);
// The 0-based index of the mixed slot in `project_config` that blends `components` (1-based physical
// filaments) in `weights` at a fixed ratio, or -1 when there is none. Matched on the stored text, as
// format_mixed_components() and format_mixed_ratios() write it. A gradient slot never matches: its
// ratio drifts from layer to layer, so it is not the blend asked for even where its stored ratios
// are the same.
int find_fixed_mixed_filament(const ConfigBase &project_config,
const std::vector<unsigned int> &components,
const std::vector<int> &weights);
// Returns true if any element in is_mixed is true.
// ConfigOptionBools stores values as std::vector<unsigned char>.
bool has_any_mixed_filament(const std::vector<unsigned char> &is_mixed);
+64 -18
View File
@@ -22,6 +22,8 @@
#include "../PrintConfig.hpp"
#include "../Surface.hpp"
#include <tbb/parallel_for.h>
#include "AABBTreeLines.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -311,6 +313,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 +360,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 +396,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;
}
@@ -664,24 +677,28 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
if (!line_based_pattern) {
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing);
for (const ExPolygon &expolygon : fill.expolygons) {
// Each expolygon is split on its own, so they run in parallel and are collected in their original order.
std::vector<std::pair<ExPolygons, ExPolygons>> split_parts(fill.expolygons.size()); // normal, narrow
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t idx) {
const ExPolygon &expolygon = fill.expolygons[idx];
Polygons filled_area = to_polygons(expolygon);
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon.
Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
if (inner_area.empty()) {
narrow_infill.emplace_back(expolygon);
continue;
split_parts[idx].second.emplace_back(expolygon);
return;
}
ExPolygons inner_ex = union_ex(inner_area);
ExPolygons expolys{expolygon};
ExPolygons narrow_ex = diff_ex(expolys, inner_ex);
ExPolygons normal_ex = intersection_ex(expolys, inner_ex);
append(normal_infill, normal_ex); // normal infill area
append(narrow_infill, narrow_ex); // narrow infill area
split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
});
for (auto &[normal_ex, narrow_ex] : split_parts) {
append(normal_infill, std::move(normal_ex));
append(narrow_infill, std::move(narrow_ex));
}
return;
@@ -703,7 +720,10 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
const double aligning_angle = -base_angle + PI;
for (const ExPolygon &expolygon : fill.expolygons) {
// Each expolygon is reconstructed on its own, so they run in parallel and are collected in their original order.
std::vector<Polygons> split_reconstructed(fill.expolygons.size());
tbb::parallel_for(size_t(0), fill.expolygons.size(), [&](size_t expolygon_idx) {
const ExPolygon &expolygon = fill.expolygons[expolygon_idx];
Polygons filled_area = to_polygons(expolygon);
polygons_rotate(filled_area, aligning_angle);
BoundingBox bb = get_extents(filled_area);
@@ -834,8 +854,10 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
}
polygons_append(normal_fill_areas, reconstructed_area);
}
split_reconstructed[expolygon_idx] = std::move(reconstructed_area);
});
for (Polygons &reconstructed_area : split_reconstructed)
polygons_append(normal_fill_areas, std::move(reconstructed_area));
polygons_rotate(normal_fill_areas, -aligning_angle);
@@ -994,15 +1016,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 +1384,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 +1434,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
@@ -1450,7 +1484,15 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
// Only the part of the layer-wide no-overlap area under this expolygon matters, so clip it to the
// expolygon's box first (padded past the safety offset, which grows the clip side). The result is
// identical; the cost is not: a layer split into many small fills, e.g. by colour painting,
// otherwise intersects every one of them with the whole layer.
BoundingBox no_overlap_bbox = get_extents(expoly);
no_overlap_bbox.offset(SCALED_EPSILON);
f->no_overlap_expolygons = intersection_ex(
ClipperUtils::clip_clipper_polygons_with_subject_bbox(surface_fill.no_overlap_expolygons, no_overlap_bbox),
ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis);
@@ -1575,6 +1617,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 +1657,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
+9
View File
@@ -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;
+26 -5
View File
@@ -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 = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
// omega >= 1 always, so fz >= baseline -> shorter vertical wavelength ->
+823
View File
@@ -0,0 +1,823 @@
#include "FillTpmsAdaptive.hpp"
#include <algorithm>
#include <array>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <deque>
#include <functional>
#include <limits>
#include <utility>
#include <vector>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include "../BoundingBox.hpp"
#include "../ClipperUtils.hpp"
#include "../ExPolygon.hpp"
#include "FillBase.hpp"
#include "../Execution/ExecutionTBB.hpp"
#include "../MarchingSquares.hpp"
#include "../Point.hpp"
#include "../Polygon.hpp"
#include "../Polyline.hpp"
#include "../PrintConfig.hpp"
#include "../libslic3r.h"
namespace Slic3r {
namespace {
// At most 4 MB of body indices; finer cells would not change the grading.
constexpr double MaxNodes = double(1 << 20);
constexpr double MinCellSize = 0.5;
// Two deepest points are in separate lobes when the depth between them drops below this ratio of the shallower one.
constexpr double NeckRatio = 0.8;
// Lobes shallower than this ratio of the deepest one of their body are graded as part of it.
constexpr double MinLobeRatio = 0.3;
// A lobe reaches twice as far as the side towards its neighbour, so that the side is half way to the surface.
constexpr double LobeReach = 2.;
// Width of the morph between the patterns of two lobes, in their distance to the center over its depth.
constexpr double LobeMorph = 0.1;
// Densities of the levels of Stepped shells, and of Smooth blend, are at most these ratios apart. Fewer levels blend
// with fewer lines running along the blends.
constexpr double ShellRatio = 1.5;
constexpr double BlendRatio = 2.5;
// Distance warp: samples of the mean depth along a ray.
constexpr int Samples = 32;
constexpr float InfF = std::numeric_limits<float>::infinity();
constexpr double InfD = std::numeric_limits<double>::infinity();
// Squared distance transform of a line (Felzenszwalb & Huttenlocher); infinite samples are no sites.
void distance_transform_line(const float *f, float *d, int n, int *v, double *s)
{
int k = -1;
for (int q = 0; q < n; ++q) {
if (f[q] == InfF)
continue;
double x = -InfD;
while (k >= 0) {
x = (f[q] + double(q) * q - f[v[k]] - double(v[k]) * v[k]) / (2. * (q - v[k]));
if (x > s[k])
break;
--k;
}
if (k < 0)
x = -InfD;
v[++k] = q;
s[k] = x;
s[k + 1] = InfD;
}
if (k < 0) {
std::fill(d, d + n, InfF);
return;
}
for (int q = 0, j = 0; q < n; ++q) {
while (s[j + 1] < q)
++j;
d[q] = float(sqr(double(q - v[j])) + f[v[j]]);
}
}
void distance_transform_axis(std::vector<float> &grid, const Vec3i32 &size, int axis, const std::function<void()> &throw_if_canceled)
{
const int n = size[axis];
const int a1 = (axis + 1) % 3;
const int a2 = (axis + 2) % 3;
const size_t stride = axis == 0 ? 1 : axis == 1 ? size_t(size.x()) : size_t(size.x()) * size.y();
tbb::parallel_for(tbb::blocked_range<size_t>(0, size_t(size[a1]) * size[a2]), [&](const tbb::blocked_range<size_t> &range) {
std::vector<float> f(n), d(n);
std::vector<int> v(n);
std::vector<double> s(n + 1);
for (size_t line = range.begin(); line < range.end(); ++line) {
Vec3i32 idx;
idx[axis] = 0;
idx[a1] = int(line % size[a1]);
idx[a2] = int(line / size[a1]);
const size_t first = (size_t(idx.z()) * size.y() + idx.y()) * size.x() + idx.x();
for (int i = 0; i < n; ++i)
f[i] = grid[first + i * stride];
distance_transform_line(f.data(), d.data(), n, v.data(), s.data());
for (int i = 0; i < n; ++i)
grid[first + i * stride] = d[i];
}
throw_if_canceled();
});
}
// Marks the nodes inside the expolygons with infinity, by even-odd scanlines.
void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, int nx, int ny, float *nodes)
{
std::vector<std::vector<double>> crossings(ny);
auto add_crossings = [&](const Polygon &polygon) {
const Points &pts = polygon.points;
for (size_t i = 0; i < pts.size(); ++i) {
const Vec2d a = unscaled(pts[i]);
const Vec2d b = unscaled(pts[i + 1 == pts.size() ? 0 : i + 1]);
if (a.y() == b.y())
continue;
const auto [lo, hi] = std::minmax(a.y(), b.y());
const int j0 = std::max(0, int(std::ceil((lo - origin.y()) / cell)));
const int j1 = std::min(ny, int(std::ceil((hi - origin.y()) / cell)));
for (int j = j0; j < j1; ++j) {
const double y = origin.y() + j * cell;
crossings[j].push_back(a.x() + (b.x() - a.x()) * (y - a.y()) / (b.y() - a.y()));
}
}
};
for (const ExPolygon &expolygon : expolygons) {
add_crossings(expolygon.contour);
for (const Polygon &hole : expolygon.holes)
add_crossings(hole);
}
for (int j = 0; j < ny; ++j) {
std::vector<double> &xs = crossings[j];
std::sort(xs.begin(), xs.end());
for (size_t k = 0; k + 1 < xs.size(); k += 2) {
const int i0 = std::max(0, int(std::ceil((xs[k] - origin.x()) / cell)));
const int i1 = std::min(nx, int(std::ceil((xs[k + 1] - origin.x()) / cell)));
std::fill(nodes + size_t(j) * nx + std::min(i0, i1), nodes + size_t(j) * nx + i1, InfF);
}
}
}
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
{
switch (gradient) {
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
default: return 1. + (ratio - 1.) * depth;
}
}
// Levels of Stepped shells and Smooth blend, geometric from the surface at 0 to the interior at count, and the
// continuous level of the target of a depth.
struct DensityLevels
{
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
{}
double scale(int level) const { return count == 0 ? 1. : std::pow(ratio, double(level) / count); }
double level(double depth) const
{
return count == 0 ? 0. : count * std::log(target_scale(ratio, gradient, depth)) / std::log(ratio);
}
double ratio;
TpmsAdaptiveGradient gradient;
int count;
};
// Scale of the pattern around the center at a radial coordinate t. The mean cell scale over the ball of radius t,
// t^-3 * integral of 3 t'^2 * target(t'), or over the disc in 2D, follows the gradient; beyond the surface the target
// is the surface scale.
class RadialScale
{
public:
RadialScale(const AdaptiveTpms &tpms, int dimensions) : m_dimensions(dimensions)
{
const double ratio = std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3);
auto target = [&tpms, ratio](double depth) { return target_scale(ratio, tpms.gradient, depth); };
m_scale[0] = target(1.);
double volume = 0.;
for (size_t i = 1; i < m_scale.size(); ++i) {
const double t0 = double(i - 1) / double(m_scale.size() - 1);
const double t1 = double(i) / double(m_scale.size() - 1);
volume += (std::pow(t1, m_dimensions) - std::pow(t0, m_dimensions)) * target(1. - 0.5 * (t0 + t1));
m_scale[i] = volume / std::pow(t1, m_dimensions);
}
}
double operator()(double t) const
{
if (t >= 1.) {
const double volume = std::pow(t, m_dimensions);
return (m_scale.back() + volume - 1.) / volume;
}
const double x = t * double(m_scale.size() - 1);
const size_t i = std::min(size_t(x), m_scale.size() - 2);
return m_scale[i] + (m_scale[i + 1] - m_scale[i]) * (x - double(i));
}
private:
int m_dimensions;
std::array<double, 257> m_scale;
};
} // namespace
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled)
: m_mode(mode)
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
{
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
// Padded by a node on each side, so that the border of the grid is outside.
m_cell = std::max(MinCellSize, std::cbrt(extent.prod() / MaxNodes));
auto nodes = [this](double length) { return int(std::ceil(length / m_cell)) + 3; };
while (double(nodes(extent.x())) * nodes(extent.y()) * nodes(extent.z()) > MaxNodes)
m_cell *= 1.1;
m_size = Vec3i32(nodes(extent.x()), nodes(extent.y()), nodes(extent.z()));
m_origin = min - Vec3d::Constant(m_cell);
const size_t sy = size_t(m_size.x());
const size_t sz = sy * m_size.y();
std::vector<float> depth(sz * m_size.z(), 0.f);
tbb::parallel_for(tbb::blocked_range<int>(0, m_size.z()), [&](const tbb::blocked_range<int> &range) {
for (int k = range.begin(); k < range.end(); ++k) {
const double z = m_origin.z() + k * m_cell;
auto it = std::lower_bound(slices.begin(), slices.end(), z, [](const Slice &s, double z) { return s.top_z < z; });
if (it != slices.end() && z > it->bottom_z)
rasterize(*it->expolygons, m_origin.head<2>(), m_cell, m_size.x(), m_size.y(), depth.data() + k * sz);
}
throw_if_canceled();
});
for (int axis = 0; axis < 3; ++axis)
if (axis != m_axis)
distance_transform_axis(depth, m_size, axis, throw_if_canceled);
auto position = [this, sy, sz](size_t i) {
return Vec3d(m_origin + m_cell * Vec3d(double(i % sy), double(i / sy % m_size.y()), double(i / sz)));
};
const std::array<std::ptrdiff_t, 6> steps{1, -1, std::ptrdiff_t(sy), -std::ptrdiff_t(sy), std::ptrdiff_t(sz), -std::ptrdiff_t(sz)};
auto node_of = [this, sy, sz](const Vec3d &pt) -> std::ptrdiff_t {
const Vec3d f = (pt - m_origin) / m_cell;
const long x = std::lround(f.x()), y = std::lround(f.y()), z = std::lround(f.z());
if (x < 0 || y < 0 || z < 0 || x >= m_size.x() || y >= m_size.y() || z >= m_size.z())
return -1;
return std::ptrdiff_t(size_t(z) * sz + size_t(y) * sy + size_t(x));
};
// In the 2D modes, the steps within a section.
auto in_section = [this](size_t step) { return int(step / 2) != m_axis; };
std::vector<std::ptrdiff_t> neighbours;
for (int dz = -1; dz <= 1; ++dz)
for (int dy = -1; dy <= 1; ++dy)
for (int dx = -1; dx <= 1; ++dx)
if ((dx != 0 || dy != 0 || dz != 0) && (m_axis < 0 || Vec3i32(dx, dy, dz)[m_axis] == 0))
neighbours.push_back(std::ptrdiff_t(dz) * std::ptrdiff_t(sz) + std::ptrdiff_t(dy) * std::ptrdiff_t(sy) + dx);
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
// is the node nearest to the middle of the tied nodes.
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
m_body.assign(depth.size(), -1);
std::vector<size_t> body_nodes;
for (size_t seed = 0; seed < depth.size(); ++seed) {
if (depth[seed] == 0.f || m_body[seed] >= 0)
continue;
const int id = int(m_bodies.size());
body_nodes.assign(1, seed);
m_body[seed] = id;
float max_depth = 0.f;
for (size_t k = 0; k < body_nodes.size(); ++k) {
max_depth = std::max(max_depth, depth[body_nodes[k]]);
for (size_t s = 0; s < steps.size(); ++s)
if (const size_t j = body_nodes[k] + steps[s]; in_section(s) && depth[j] > 0.f && m_body[j] < 0) {
m_body[j] = id;
body_nodes.push_back(j);
}
}
m_bodies.push_back({m_lobes.size(), 0, (std::sqrt(double(max_depth)) - 0.5) * m_cell});
if (!lobes)
continue;
std::vector<size_t> peaks;
for (size_t i : body_nodes)
if (depth[i] >= sqr(MinLobeRatio) * max_depth &&
std::all_of(neighbours.begin(), neighbours.end(), [&](std::ptrdiff_t n) { return depth[i + n] <= depth[i]; }))
peaks.push_back(i);
std::sort(peaks.begin(), peaks.end(), [&depth](size_t a, size_t b) { return depth[a] > depth[b] || (depth[a] == depth[b] && a < b); });
auto necked = [&](size_t a, size_t b) {
const Vec3d pa = position(a), pb = position(b);
const double limit = sqr(NeckRatio) * std::min(depth[a], depth[b]);
const int samples = int(std::ceil((pb - pa).norm() / (0.5 * m_cell)));
for (int s = 1; s < samples; ++s)
if (depth[node_of(pa + (pb - pa) * (double(s) / samples))] < limit)
return true;
return false;
};
// A peak joins the first lobe it sees without a neck, if it is as deep. The lobes are made one at a time,
// from the first remaining peak, testing the others in parallel.
std::vector<std::vector<size_t>> ties;
while (!peaks.empty()) {
const size_t front = peaks.front();
std::vector<char> joins(peaks.size(), 0);
tbb::parallel_for(tbb::blocked_range<size_t>(1, peaks.size()), [&](const tbb::blocked_range<size_t> &range) {
for (size_t k = range.begin(); k < range.end(); ++k)
if (!necked(front, peaks[k]))
joins[k] = std::sqrt(depth[peaks[k]]) >= std::sqrt(depth[front]) - 1.f ? 1 : 2;
});
std::vector<size_t> &tied = ties.emplace_back(1, front);
std::vector<size_t> remaining;
for (size_t k = 1; k < peaks.size(); ++k)
if (joins[k] == 0)
remaining.push_back(peaks[k]);
else if (joins[k] == 1)
tied.push_back(peaks[k]);
peaks = std::move(remaining);
}
m_bodies.back().lobes = ties.size();
for (const std::vector<size_t> &tied : ties) {
Vec3d middle(0., 0., 0.);
for (size_t i : tied)
middle += position(i);
middle /= double(tied.size());
Vec3d center = position(tied.front());
for (size_t i : tied)
if ((position(i) - middle).squaredNorm() < (center - middle).squaredNorm())
center = position(i);
m_lobes.push_back({center, (std::sqrt(double(depth[tied.front()])) - 0.5) * m_cell, {}});
}
}
throw_if_canceled();
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
m_depth.assign(depth.size(), 0.f);
for (size_t i = 0; i < depth.size(); ++i)
if (m_body[i] >= 0)
m_depth[i] = float(std::min(1., std::max(0., std::sqrt(double(depth[i])) - 0.5) * m_cell / m_bodies[m_body[i]].depth));
}
// The reach of a lobe is the first exit along each direction from its center, smoothed over the directions.
// It is shortened towards a neighbouring lobe, from where the point is nearer to the other lobe relative to their depths.
std::vector<int> lobe_body(m_lobes.size());
for (size_t id = 0; id < m_bodies.size(); ++id)
std::fill_n(lobe_body.begin() + m_bodies[id].first_lobe, m_bodies[id].lobes, int(id));
const int rows = this->directions() / Azimuth;
auto direction = [this](int i, int j) {
const double azimuth = j * 2. * PI / Azimuth;
Vec3d dir = Vec3d::Zero();
if (m_axis < 0) {
const double polar = (i + 0.5) * PI / Polar;
dir = Vec3d(std::sin(polar) * std::cos(azimuth), std::sin(polar) * std::sin(azimuth), std::cos(polar));
} else {
dir[(m_axis + 1) % 3] = std::cos(azimuth);
dir[(m_axis + 2) % 3] = std::sin(azimuth);
}
return dir;
};
// Two passes of a box filter over the neighbouring directions, for each of the values of a direction.
auto smooth = [rows](std::vector<double> &values, size_t count) {
for (int pass = 0; pass < 2; ++pass) {
std::vector<double> smoothed(values.size(), 0.);
for (int i = 0; i < rows; ++i)
for (int j = 0; j < Azimuth; ++j)
for (size_t k = 0; k < count; ++k) {
double &sum = smoothed[size_t(i * Azimuth + j) * count + k];
for (int di = -1; di <= 1; ++di)
for (int dj = -1; dj <= 1; ++dj)
sum += values[size_t(std::clamp(i + di, 0, rows - 1) * Azimuth + (j + dj + Azimuth) % Azimuth) * count + k];
sum /= 9.;
}
values = std::move(smoothed);
}
};
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_lobes.size()), [&](const tbb::blocked_range<size_t> &range) {
for (size_t l = range.begin(); l < range.end(); ++l) {
const int id = lobe_body[l];
const Body &body = m_bodies[id];
Lobe &lobe = m_lobes[l];
// The other lobes of the body, by the distance from the center beyond which they may be nearer.
std::vector<std::pair<double, size_t>> others;
for (size_t k = body.first_lobe; k < body.first_lobe + body.lobes; ++k)
if (k != l)
others.emplace_back((m_lobes[k].center - lobe.center).norm() / (1. + m_lobes[k].depth / lobe.depth), k);
std::sort(others.begin(), others.end());
auto nearer_lobe = [&](const Vec3d &pt, double r) {
for (auto it = others.begin(); it != others.end() && it->first < r; ++it)
if ((pt - m_lobes[it->second].center).norm() / m_lobes[it->second].depth < r / lobe.depth)
return true;
return false;
};
const double step = 0.5 * m_cell;
std::vector<double> log_reach(this->directions());
for (int i = 0; i < rows; ++i)
for (int j = 0; j < Azimuth; ++j) {
const Vec3d dir = direction(i, j);
double r = 0.;
double limit = InfD;
for (;;) {
const Vec3d pt = lobe.center + (r + step) * dir;
const std::ptrdiff_t n = node_of(pt);
if (n < 0 || depth[n] == 0.f || m_body[n] != id || r + step >= limit)
break;
if (limit == InfD && nearer_lobe(pt, r + step))
limit = LobeReach * (r + step);
r += step;
}
log_reach[i * Azimuth + j] = std::log(std::min(r + 0.5 * step, limit));
}
smooth(log_reach, 1);
lobe.reach.resize(log_reach.size());
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
std::vector<double> mean(log_reach.size() * (Samples + 1));
for (size_t d = 0; d < log_reach.size(); ++d) {
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
double integral = 0.;
double previous = this->depth(lobe.center);
mean[d * (Samples + 1)] = previous;
for (int k = 1; k <= Samples; ++k) {
// Exact for a depth linear between the samples, a + b * tau, weighted by tau^2.
const double t0 = double(k - 1) / Samples;
const double t1 = double(k) / Samples;
const double d1 = this->depth(lobe.center + t1 * lobe.reach[d] * dir);
const double b = (d1 - previous) * Samples;
const double a = previous - b * t0;
integral += a * (std::pow(t1, 3) - std::pow(t0, 3)) / 3. + b * (std::pow(t1, 4) - std::pow(t0, 4)) / 4.;
previous = d1;
mean[d * (Samples + 1) + k] = 3. * integral / std::pow(t1, 3);
}
}
// Smoothed like the reach: sharper profiles shear the pattern across the layer, adding lines.
smooth(mean, Samples + 1);
lobe.mean_depth.assign(mean.begin(), mean.end());
}
}
throw_if_canceled();
});
// Every other node belongs to its nearest body, within its section in the 2D modes.
if (m_axis >= 0) {
std::vector<bool> has_body(m_size[m_axis], false);
for (size_t i = 0; i < m_body.size(); ++i)
if (m_body[i] >= 0)
has_body[m_axis == 0 ? i % sy : m_axis == 1 ? i / sy % m_size.y() : i / sz] = true;
m_section.assign(m_size[m_axis], -1);
for (int k = 0; k < m_size[m_axis]; ++k)
for (int d = 0; d < m_size[m_axis] && m_section[k] < 0; ++d)
if (k - d >= 0 && has_body[k - d])
m_section[k] = k - d;
else if (k + d < m_size[m_axis] && has_body[k + d])
m_section[k] = k + d;
}
// Stepped shells and Smooth blend only look up the depth.
if (!lobes) {
m_body = {};
return;
}
if (m_bodies.size() == 1) {
std::fill(m_body.begin(), m_body.end(), 0);
return;
}
std::deque<size_t> queue;
for (size_t i = 0; i < m_body.size(); ++i)
if (m_body[i] >= 0)
queue.push_back(i);
while (!queue.empty()) {
const size_t i = queue.front();
queue.pop_front();
const size_t x = i % sy, y = i / sy % m_size.y(), z = i / sz;
const std::array<bool, 6> valid{x + 1 < sy, x > 0, y + 1 < size_t(m_size.y()), y > 0, z + 1 < size_t(m_size.z()), z > 0};
for (size_t k = 0; k < steps.size(); ++k)
if (valid[k] && in_section(k) && m_body[i + steps[k]] < 0) {
m_body[i + steps[k]] = m_body[i];
queue.push_back(i + steps[k]);
}
}
}
double TpmsRadialField::depth(const Vec3d &pt) const
{
assert(!m_depth.empty());
const Vec3d f = (pt - m_origin) / m_cell;
std::array<int, 3> n0;
std::array<double, 3> w;
for (int a = 0; a < 3; ++a) {
n0[a] = std::clamp(int(std::floor(f[a])), 0, m_size[a] - 2);
w[a] = std::clamp(f[a] - n0[a], 0., 1.);
}
const size_t sy = size_t(m_size.x());
const size_t sz = sy * size_t(m_size.y());
double value = 0.;
for (int c = 0; c < 8; ++c) {
const size_t n = size_t(n0[2] + (c >> 2)) * sz + size_t(n0[1] + (c >> 1 & 1)) * sy + size_t(n0[0] + (c & 1));
value += (c & 1 ? w[0] : 1. - w[0]) * (c >> 1 & 1 ? w[1] : 1. - w[1]) * (c >> 2 ? w[2] : 1. - w[2]) * m_depth[n];
}
return value;
}
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const
{
Vec3d d = pt - center;
if (m_axis >= 0)
d[m_axis] = 0.;
return d;
}
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
{
const Vec3d d = this->offset(pt, lobe.center);
const double r = d.norm();
// Distance warp: the radial coordinate of the linear profile with the same mean depth, which is 1 - 3/4 of it.
auto warp = [](double mean_depth) { return std::max(0., 4. / 3. * (1. - mean_depth)); };
if (r < EPSILON)
return lobe.mean_depth.empty() ? 0. : warp(lobe.mean_depth.front());
int i = 0;
double fi = 0.;
double azimuth;
if (m_axis < 0) {
const double polar = std::clamp(std::acos(std::clamp(d.z() / r, -1., 1.)) / PI * Polar - 0.5, 0., double(Polar - 1));
i = std::min(int(polar), Polar - 2);
fi = polar - i;
azimuth = std::atan2(d.y(), d.x());
} else
azimuth = std::atan2(d[(m_axis + 2) % 3], d[(m_axis + 1) % 3]);
azimuth *= Azimuth / (2. * PI);
if (azimuth < 0.)
azimuth += Azimuth;
const int j0 = int(azimuth) % Azimuth;
const int j1 = (j0 + 1) % Azimuth;
const double fj = azimuth - std::floor(azimuth);
auto at = [&lobe](int i, int j) { return double(lobe.reach[i * Azimuth + j]); };
double reach = at(i, j0) * (1. - fj) + at(i, j1) * fj;
if (fi > 0.)
reach = reach * (1. - fi) + (at(i + 1, j0) * (1. - fj) + at(i + 1, j1) * fj) * fi;
const double tau = r / reach;
if (lobe.mean_depth.empty())
return tau;
// Beyond the surface, the depth is zero.
auto mean_at = [&lobe, tau](int i, int j) {
const float *mean = lobe.mean_depth.data() + size_t(i * Azimuth + j) * (Samples + 1);
if (tau >= 1.)
return double(mean[Samples]) / (tau * tau * tau);
const double x = tau * Samples;
const int k = std::min(int(x), Samples - 1);
return mean[k] + (mean[k + 1] - mean[k]) * (x - k);
};
double mean = mean_at(i, j0) * (1. - fj) + mean_at(i, j1) * fj;
if (fi > 0.)
mean = mean * (1. - fi) + (mean_at(i + 1, j0) * (1. - fj) + mean_at(i + 1, j1) * fj) * fi;
return warp(mean);
}
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
{
assert(!this->empty());
Vec3i32 idx;
for (int axis = 0; axis < 3; ++axis)
idx[axis] = std::clamp<int>(int(std::lround((pt[axis] - m_origin[axis]) / m_cell)), 0, m_size[axis] - 1);
auto node = [this](const Vec3i32 &idx) { return (size_t(idx.z()) * m_size.y() + idx.y()) * m_size.x() + idx.x(); };
if (m_axis < 0)
return this->body_radial(node(idx), pt, 1.f, out.data());
// The sections around pt, or the nearest ones with a body.
const double f = std::clamp((pt[m_axis] - m_origin[m_axis]) / m_cell, 0., double(m_size[m_axis] - 1));
const int k = std::min(int(f), m_size[m_axis] - 2);
const float w = float(f - k);
size_t count = 0;
if (w < 1.f) {
idx[m_axis] = m_section[k];
count += this->body_radial(node(idx), pt, 1.f - w, out.data());
}
if (w > 0.f) {
idx[m_axis] = m_section[k + 1];
count += this->body_radial(node(idx), pt, w, out.data() + count);
}
return count;
}
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
{
const Body &body = m_bodies[m_body[node]];
if (body.lobes == 1) {
const Lobe &lobe = m_lobes[body.first_lobe];
out[0] = {lobe.center, radial(lobe, pt), weight};
return 1;
}
// The lobes nearest relative to their depth; they morph into each other near the sides where they are as near.
std::array<std::pair<double, size_t>, MaxMorph> nearest;
size_t count = 0;
for (size_t l = body.first_lobe; l < body.first_lobe + body.lobes; ++l) {
const double d = this->offset(pt, m_lobes[l].center).norm() / m_lobes[l].depth;
if (count < MaxMorph)
nearest[count++] = {d, l};
else if (d < nearest.back().first)
nearest.back() = {d, l};
else
continue;
for (size_t k = count - 1; k > 0 && nearest[k].first < nearest[k - 1].first; --k)
std::swap(nearest[k], nearest[k - 1]);
}
std::array<double, MaxMorph> blend;
double total = 0.;
size_t morphs = 0;
for (; morphs < count; ++morphs) {
const double u = 0.5 - (nearest[morphs].first - nearest[0].first) / LobeMorph;
if (u <= 0.)
break;
blend[morphs] = u * u * (3. - 2. * u);
total += blend[morphs];
}
for (size_t k = 0; k < morphs; ++k) {
const Lobe &lobe = m_lobes[nearest[k].second];
out[k] = {lobe.center, radial(lobe, pt), float(weight * blend[k] / total)};
}
return morphs;
}
} // namespace Slic3r
namespace marchsq {
using namespace Slic3r;
struct AdaptiveTpmsField
{
static constexpr float gsizef = 0.40f; // grid cell size in mm (roughly line segment length).
static constexpr float rsizef = 0.004f; // raster pixel size in mm (roughly point accuracy).
const coord_t rsize = scaled(rsizef);
const long gsize = std::lround(gsizef / rsizef);
const AdaptiveTpms &tpms;
const TpmsRadialField &radial_field;
RadialScale scale;
DensityLevels levels;
Point size;
Point offs;
double z;
double cos_angle;
double sin_angle;
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
, size(bbox.size()), offs(bbox.min), z(z)
, cos_angle(std::cos(angle)), sin_angle(std::sin(angle))
{}
// The pattern is scaled around the center of the lobe, morphing into the pattern of a neighbouring lobe near the
// side between them. In the 2D modes only within the section, with the interior frequency along the axis.
// The radial field is in the object frame, the fill is rotated by -angle.
float get_scalar(const Coord &p) const
{
const Point pt = to_Point(p);
const double x = unscaled(pt.x());
const double y = unscaled(pt.y());
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
auto lattice = [this, x, y](int level) {
const double frequency = tpms.surface_frequency * levels.scale(level);
return tpms.equation(float(frequency * x), float(frequency * y), float(frequency * z));
};
if (levels.count == 0)
return lattice(0);
const double c = std::clamp(levels.level(radial_field.depth(obj)), 0., double(levels.count));
const int k = std::min(int(c), levels.count - 1);
const double u = c - k;
const double w = u * u * (3. - 2. * u);
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
}
const int axis = radial_field.axis();
TpmsRadialField::Radials radials;
const size_t count = radial_field.radial(obj, radials);
float value = 0.f;
for (size_t i = 0; i < count; ++i) {
const auto &[center, t, weight] = radials[i];
Vec3d q = tpms.surface_frequency * scale(t) * (obj - center);
if (axis >= 0)
q[axis] = tpms.interior_frequency * obj[axis];
value += weight * tpms.equation(float(cos_angle * q.x() + sin_angle * q.y()), float(cos_angle * q.y() - sin_angle * q.x()), float(q.z()));
}
return value;
}
inline coord_t to_coord(long x) const { return x * rsize; }
inline long to_coordr(coord_t x) const { return x / rsize; }
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
};
template<> struct _RasterTraits<AdaptiveTpmsField>
{
using ValueType = float;
static float get(const AdaptiveTpmsField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const AdaptiveTpmsField &sf) { return sf.to_coordr(sf.size.x()); }
};
// Continuous density level of the depth over a layer, in the object frame.
struct TpmsLevelField
{
static constexpr float gsizef = 0.5f;
static constexpr float rsizef = 0.05f;
const coord_t rsize = scaled(rsizef);
const long gsize = std::lround(gsizef / rsizef);
const TpmsRadialField &field;
const DensityLevels &levels;
Point size;
Point offs;
double z;
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
{}
float get_scalar(const Coord &p) const
{
const Point pt = to_Point(p);
return float(levels.level(field.depth(Vec3d(unscaled(pt.x()), unscaled(pt.y()), z))));
}
inline coord_t to_coord(long x) const { return x * rsize; }
inline long to_coordr(coord_t x) const { return x / rsize; }
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
};
template<> struct _RasterTraits<TpmsLevelField>
{
using ValueType = float;
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
};
} // namespace marchsq
namespace Slic3r {
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
{
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
const coord_t cell = scaled(marchsq::AdaptiveTpmsField::gsizef);
bbox.offset(cell);
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
const marchsq::AdaptiveTpmsField raster(tpms, field, bbox, z - 0.5 * layer_height, angle);
const std::vector<marchsq::Ring> rings = marchsq::execute_with_policy(ex_tbb, raster, 0.f, {raster.gsize, raster.gsize});
// Loops narrower than two lines print as blobs.
const double min_loop_length = scaled(2. * PI * spacing);
Polylines polylines;
polylines.reserve(rings.size());
for (const marchsq::Ring &ring : rings) {
Polyline polyline;
polyline.points.reserve(ring.size() + 1);
for (const marchsq::Coord &crd : ring)
polyline.points.emplace_back(raster.to_Point(crd));
polyline.points.push_back(polyline.points.front());
polyline.simplify(SCALED_SPARSE_INFILL_RESOLUTION);
if (polyline.length() >= min_loop_length)
polylines.push_back(std::move(polyline));
}
return polylines;
}
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
{
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
if (levels.count == 0)
return {{surface_density, {expolygon}}};
// A fixed sampling grid, so that every region of a layer gets the same shells.
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
BoundingBox bbox = get_extents(expolygon);
bbox.offset(cell);
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
const marchsq::TpmsLevelField raster(field, levels, bbox, z);
// Each level takes the part deeper than the middle between it and the previous one.
std::vector<TpmsShell> shells;
ExPolygons remaining{expolygon};
for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
Polygons deeper;
for (const marchsq::Ring &ring : marchsq::execute_with_policy(ex_tbb, raster, float(level + 0.5), {raster.gsize, raster.gsize})) {
Polygon &polygon = deeper.emplace_back();
polygon.points.reserve(ring.size());
for (const marchsq::Coord &crd : ring)
polygon.points.emplace_back(raster.to_Point(crd));
}
ExPolygons inner = intersection_ex(union_ex(deeper), remaining);
shells.push_back({float(surface_density * levels.scale(level)), diff_ex(remaining, inner)});
remaining = std::move(inner);
}
if (!remaining.empty())
shells.push_back({interior_density, std::move(remaining)});
return shells;
}
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
{
FillParams shell_params = params;
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
shell_params.density = shell.density;
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
fill_shell(shell_params, part);
}
}
} // namespace Slic3r
+143
View File
@@ -0,0 +1,143 @@
#pragma once
#include <array>
#include <cstddef>
#include <functional>
#include <memory>
#include <utility>
#include <vector>
#include "../libslic3r.h"
#include "FillBase.hpp"
#include "../BoundingBox.hpp"
#include "../ExPolygon.hpp"
#include "../Point.hpp"
#include "../Polyline.hpp"
#include "../PrintConfig.hpp"
namespace Slic3r {
// Radial coordinate inside the lobes of the bodies of an object, sampled from its slices: 0 at the center of a
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
class TpmsRadialField
{
public:
struct Slice
{
coordf_t bottom_z;
coordf_t top_z;
const ExPolygons *expolygons;
};
struct Radial
{
Vec3d center;
double t;
float weight;
};
// Slices sorted by z, in the XY coordinates of the fill and the print Z.
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled);
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
static constexpr size_t MaxMorph = 4;
using Radials = std::array<Radial, 2 * MaxMorph>;
// Without a body, as when the object is thinner than the grid cells.
bool empty() const { return m_bodies.empty(); }
// Radial coordinates of pt in unscaled coordinates towards the lobe it belongs to, and towards the neighbouring
// lobes near the sides between them, with weights summing to 1. In the 2D modes, those of the two sections around
// pt. Returns their count.
size_t radial(const Vec3d &pt, Radials &out) const;
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
int axis() const { return m_axis; }
TpmsAdaptiveMode mode() const { return m_mode; }
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
// the surface to 1.
double depth(const Vec3d &pt) const;
private:
struct Lobe
{
Vec3d center;
double depth;
// Distance from the center to the surface on a latitude-longitude grid of directions.
std::vector<float> reach;
// Distance warp: mean depth over the ball along each direction, sampled up to the reach.
std::vector<float> mean_depth;
};
struct Body
{
size_t first_lobe;
size_t lobes;
// Distance from the deepest point to the surface.
double depth;
};
double radial(const Lobe &lobe, const Vec3d &pt) const;
size_t body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const;
// Offset of pt from a center, within the section in the 2D modes.
Vec3d offset(const Vec3d &pt, const Vec3d &center) const;
int directions() const { return m_axis < 0 ? Polar * Azimuth : Azimuth; }
// Directions of the reach of a lobe, on a latitude-longitude grid, or a circle in the 2D modes.
static constexpr int Polar = 24;
static constexpr int Azimuth = 48;
TpmsAdaptiveMode m_mode;
int m_axis;
Vec3d m_origin;
double m_cell;
Vec3i32 m_size;
// Nearest body of every grid node.
std::vector<int> m_body;
std::vector<Body> m_bodies;
std::vector<Lobe> m_lobes;
// In the 2D modes, the nearest section with a body to every section.
std::vector<int> m_section;
// In the modes following the distance to the surface, the depth of every grid node.
std::vector<float> m_depth;
};
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
// A field for every adaptive mode in use, indexed by the mode.
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
struct AdaptiveTpms
{
// Implicit TPMS equation with a period of 2 PI.
float (*equation)(float x, float y, float z);
// Pattern frequencies at the surface and at the center, in radians per mm.
double surface_frequency;
double interior_frequency;
TpmsAdaptiveGradient gradient;
};
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
struct TpmsShell
{
float density;
ExPolygons expolygons;
};
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
// middle of the layer.
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
// so the lines connected along the boundaries of two shells don't overlap.
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r
+43 -18
View File
@@ -14,8 +14,10 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/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 = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
BoundingBox bbox = expolygon.contour.bounding_box();
bbox.offset(scale_((params.multiline + 1) * this->spacing));
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
} 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);
+33 -15
View File
@@ -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 = [&params, 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);
+77 -5
View File
@@ -140,15 +140,87 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// For many islands, the same scan with the edges binned in a uniform grid over their boxes, so each edge is only tested against the edges sharing a
// cell with it - two edges whose boxes overlap always do. It returns the same crossing as the all-pairs scan
// (smallest i, then smallest j), so the result is unchanged. The all-pairs scan is quadratic in the edge count and
// runs again after every reversal; rebuilding the grid costs more than it saves below the threshold.
constexpr size_t grid_min_size = 500;
BoundingBox extent;
for (size_t idx : path)
extent.merge(centers[idx]);
const int grid_n = std::clamp(int(std::sqrt(double(pn))), 1, 256);
const coord_t cell_w = std::max<coord_t>(1, (extent.max.x() - extent.min.x()) / grid_n + 1);
const coord_t cell_h = std::max<coord_t>(1, (extent.max.y() - extent.min.y()) / grid_n + 1);
const auto for_cells = [&](const Point& a, const Point& b, auto&& fn) {
const int x0 = int((std::min(a.x(), b.x()) - extent.min.x()) / cell_w), x1 = int((std::max(a.x(), b.x()) - extent.min.x()) / cell_w);
const int y0 = int((std::min(a.y(), b.y()) - extent.min.y()) / cell_h), y1 = int((std::max(a.y(), b.y()) - extent.min.y()) / cell_h);
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
fn(y * grid_n + x);
};
std::vector<std::vector<size_t>> edge_cells(size_t(grid_n) * grid_n);
auto find_crossing_grid = [&]() -> std::pair<size_t, size_t> {
for (std::vector<size_t>& cell : edge_cells)
cell.clear();
for (size_t j = 0; j < n_edges; ++j)
for_cells(centers[path[j]], centers[path[(j + 1) % pn]], [&](int cell) { edge_cells[cell].emplace_back(j); });
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[(i + 1) % pn]];
size_t first_j = std::numeric_limits<size_t>::max();
for_cells(ai, bi, [&](int cell) {
for (size_t j : edge_cells[cell]) {
if (j < i + 2 || j >= first_j) continue;
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
if (i == 0 && j == pn - 1) continue;
const Point& aj = centers[path[j]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
first_j = j;
}
});
if (first_j != std::numeric_limits<size_t>::max())
return {i, first_j};
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
int max_iters = static_cast<int>(pn * pn);
const int max_iters = static_cast<int>(pn * pn);
bool improved = false;
while (max_iters-- > 0) {
auto [ci, cj] = find_crossing();
if (ci == std::numeric_limits<size_t>::max()) break;
improved = true;
// Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on
// islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same
// orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps
// to the ordering the capped loop would have stopped on are taken.
std::unordered_map<uint64_t, int> seen_paths; // path hash -> reversals done when it was reached
const auto path_hash = [&path]() {
uint64_t h = 1469598103934665603ull; // FNV-1a
for (size_t idx : path)
h = (h ^ uint64_t(idx)) * 1099511628211ull;
return h;
};
const auto reverse_first_crossing = [&]() {
auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing();
if (ci == std::numeric_limits<size_t>::max())
return false;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
return true;
};
seen_paths.emplace(path_hash(), 0);
for (int iter = 1; iter <= max_iters && reverse_first_crossing(); ++iter) {
improved = true;
if (auto [it, inserted] = seen_paths.emplace(path_hash(), iter); !inserted) {
for (int steps = (max_iters - iter) % (iter - it->second); steps > 0; --steps)
reverse_first_crossing();
break;
}
}
return improved;
}
+18 -13
View File
@@ -29,6 +29,7 @@
#include <memory>
#include <random>
#include <algorithm>
#include <limits>
#include <queue>
#include <string>
#include <unordered_map>
@@ -1210,21 +1211,21 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
const size_t layer_idx, const float max_distance,
const SeamPlacerImpl::SeamComparator &comparator) const {
using namespace SeamPlacerImpl;
std::vector<size_t> nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position,
max_distance);
if (nearby_points_indices.empty()) {
return {};
}
size_t best_nearby_point_index = nearby_points_indices[0];
size_t nearest_point_index = nearby_points_indices[0];
// Now find best nearby point, nearest point, and corresponding indices
for (const size_t &nearby_point_index : nearby_points_indices) {
// Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within
// the radius, so they are looked at as the search finds them rather than collected into a vector first.
constexpr size_t none = std::numeric_limits<size_t>::max();
size_t best_nearby_point_index = none;
size_t nearest_point_index = none;
visit_nearby_points(*layers[layer_idx].points_tree, projected_position, max_distance,
[&layers, &comparator, &projected_position, layer_idx, &best_nearby_point_index, &nearest_point_index]
(size_t nearby_point_index) {
if (best_nearby_point_index == none) {
// The first point found starts both, as the first of the collected ones did.
best_nearby_point_index = nearest_point_index = nearby_point_index;
}
const SeamCandidate &point = layers[layer_idx].points[nearby_point_index];
if (point.perimeter.finalized) {
continue; // skip over finalized perimeters, try to find some that is not finalized
return; // skip over finalized perimeters, try to find some that is not finalized
}
if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index],
projected_position.head<2>())
@@ -1236,6 +1237,10 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|| layers[layer_idx].points[nearest_point_index].perimeter.finalized) {
nearest_point_index = nearby_point_index;
}
});
if (best_nearby_point_index == none) {
return {};
}
const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index];
+30
View File
@@ -318,6 +318,36 @@ std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const P
return visitor.result;
}
// Visits the points within max_distance of center, in the order find_nearby_points() would collect them, and hands
// each of them to `visitor_fn` instead of returning them all: a search over a dense set spends more on collecting the
// points into a vector than on the search itself, and its caller usually keeps only a few of them.
template<typename KDTreeIndirectType, typename PointType, typename VisitorFn>
void visit_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType &max_distance, VisitorFn visitor_fn)
{
using CoordType = typename KDTreeIndirectType::CoordType;
struct Visitor {
const KDTreeIndirectType &kdtree;
const PointType center;
const CoordType max_distance_squared;
VisitorFn visitor_fn;
unsigned int operator()(size_t idx, size_t dimension) {
auto dist = CoordType(0);
for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) {
CoordType d = center[i] - kdtree.coordinate(idx, i);
dist += d * d;
}
if (dist < max_distance_squared)
visitor_fn(idx);
return kdtree.descent_mask(center[dimension], max_distance_squared, idx, dimension);
}
} visitor { kdtree, center, max_distance * max_distance, visitor_fn };
kdtree.visit(visitor);
}
template<typename KDTreeIndirectType, typename PointType>
std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType& max_distance)
+6 -2
View File
@@ -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;
+2 -1
View File
@@ -99,10 +99,11 @@ void LayerRegion::slices_to_fill_surfaces_clipped()
by_surface[size_t(surface.surface_type)].emplace_back(&surface);
// Trim surfaces by the fill_boundaries.
this->fill_surfaces.surfaces.clear();
const Polygons fill_boundaries = to_polygons(this->fill_expolygons);
for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
const SurfacesPtr &this_surfaces = by_surface[surface_type];
if (! this_surfaces.empty())
this->fill_surfaces.append(intersection_ex(this_surfaces, this->fill_expolygons), SurfaceType(surface_type));
this->fill_surfaces.append(intersection_ex_by_piece(to_expolygons(this_surfaces), fill_boundaries), SurfaceType(surface_type));
}
}
+272 -72
View File
@@ -22,6 +22,7 @@
#include "Surface.hpp"
#include "libslic3r.h"
#include <numeric>
#include <cmath>
#include <cstddef>
#include <list>
@@ -1362,10 +1363,15 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
}
#endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM
// When the upper surface of an object is occluded, it should no longer be considered the upper surface
// When the upper surface of an object is occluded, it should no longer be considered the upper surface.
// Every (colour, layer) pair is trimmed on its own, so they all run at once: the painted faces of a finely
// textured part project hundreds of thousands of triangles onto one layer, which used to be trimmed serially.
{
for (size_t extruder_idx = 0; extruder_idx < num_facets_states; ++extruder_idx) {
for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) {
const size_t occluded_pairs = num_facets_states * layers.size();
tbb::parallel_for(tbb::blocked_range<size_t>(0, occluded_pairs), [&](const tbb::blocked_range<size_t> &range) {
for (size_t pair_idx = range.begin(); pair_idx < range.end(); ++pair_idx) {
const size_t extruder_idx = pair_idx / layers.size();
const size_t layer_idx = pair_idx % layers.size();
if (!top_raw[extruder_idx].empty() && !top_raw[extruder_idx][layer_idx].empty() && layer_idx + 1 < layers.size()) {
top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]);
}
@@ -1373,7 +1379,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]);
}
}
}
});
}
std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states);
@@ -1431,11 +1437,58 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
return out;
};
// Projects a painted top or bottom face `ex` of layer `layer_idx` onto the shell layers below or above it (in
// `shell_layers`, nearest first), one more perimeter in on each, stopping at the first layer where nothing is left.
// Only the slices within the deepest offset of `ex` (three times that with the miter joins) decide the result, so the
// work is done per tile of `ex`'s ExPolygons on the slices cut to the tile's box grown by that much: the same result, but
// each Clipper call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in
// parallel.
const auto project_to_shells = [&input_expolygons](const ExPolygons &ex, size_t layer_idx, const std::vector<size_t> &shell_layers,
const LayerColorStat &stat, ShellProjections &dst) {
std::vector<float> offsets(shell_layers.size());
float offset = 0.f;
for (size_t i = 0; i < shell_layers.size(); ++i) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
offset -= (stat.extrusion_spacing + stat.extrusion_width);
offsets[i] = offset;
}
if (offsets.empty())
return;
const coord_t reach = coord_t(std::ceil(DefaultMiterLimit * std::abs(offsets.back()))) + 10 * SCALED_EPSILON;
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(ex, 16);
// [shell layer][tile]
std::vector<std::vector<ExPolygons>> shells(shell_layers.size(), std::vector<ExPolygons>(tiles.size()));
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
const BoundingBox bbox = tile.bbox.inflated(reach);
ExPolygons tile_ex;
tile_ex.reserve(tile.members.size());
for (size_t i : tile.members)
tile_ex.emplace_back(ex[i]);
Polygons layer_slices_trimmed = ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[layer_idx], bbox);
for (size_t i = 0; i < shell_layers.size() && ! layer_slices_trimmed.empty(); ++i) {
const ExPolygons trimmed = intersection_ex(layer_slices_trimmed, ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[shell_layers[i]], bbox));
shells[i][tile_idx] = opening_ex(intersection_ex(tile_ex, offset_ex(trimmed, offsets[i])), stat.small_region_threshold);
layer_slices_trimmed = to_polygons(trimmed);
}
});
for (size_t i = 0; i < shell_layers.size(); ++i) {
ExPolygons shell;
for (ExPolygons &tile_shell : shells[i])
append(shell, std::move(tile_shell));
if (shell.empty())
break;
dst.emplace_back(shell_layers[i], std::move(shell));
}
};
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
&throw_on_cancel_callback, &bottom_raw, &triangles_by_color_bottom, &project_to_shells,
&shell_triangles_by_color_top, &shell_triangles_by_color_bottom](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
// Each colour writes only its own vectors, so the colours run in parallel: a painted top or bottom face
// projects onto a single layer, which otherwise did all of its colours on one thread.
tbb::parallel_for(size_t(0), size_t(num_facets_states), [&](size_t color_idx) {
throw_on_cancel_callback();
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
@@ -1444,18 +1497,10 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (! top_ex.empty()) {
append(triangles_by_color_top[color_idx][layer_idx], top_ex);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width ;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last));
}
std::vector<size_t> shell_layers;
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx)
shell_layers.emplace_back(size_t(last_idx));
project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]);
}
}
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
@@ -1464,21 +1509,13 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
if (! bottom_ex.empty()) {
append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last));
}
std::vector<size_t> shell_layers;
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx)
shell_layers.emplace_back(last_idx);
project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]);
}
}
}
});
}
});
@@ -1499,20 +1536,23 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
&shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
throw_on_cancel_callback();
ExPolygons painted_exploys;
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
// The per-colour unions below are independent of each other, so they run in parallel (a painted top or
// bottom face puts all of its colours on one layer); whatever combines the colours stays in colour order.
const auto merge_colour_union = [&](size_t color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx];
append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx]));
append(self, std::move(triangles_by_color_top[color_idx][layer_idx]));
self = union_ex(self);
};
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), merge_colour_union);
append(painted_exploys, self);
}
ExPolygons painted_exploys;
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx)
append(painted_exploys, triangles_by_color_merged[color_idx][layer_idx]);
painted_exploys = union_ex(painted_exploys);
//BBS: merge the top and bottom shell layers
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), [&](size_t color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx];
auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys);
@@ -1521,7 +1561,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
append(self, top_area);
append(self, bottom_area);
self = union_ex(self);
}
});
// Trim one region by the other if some of the regions overlap.
ExPolygons painted_regions;
for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) {
@@ -1888,7 +1928,69 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto
}
}
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<std::vector<ExPolygons>> &segmented_regions,
// Finds the islands (layer ExPolygons) a region piece overlaps. A top or bottom region is projected from the neighbouring
// layers and may reach past the island it belongs to, or over several islands.
class IslandLocator
{
public:
explicit IslandLocator(const ExPolygons &islands) : m_islands(islands)
{
m_bboxes.reserve(islands.size());
for (const ExPolygon &island : islands) {
m_bboxes.emplace_back(get_extents(island));
m_extent.merge(m_bboxes.back());
}
if (!m_extent.defined)
return;
const Point size = m_extent.size();
m_cell_w = std::max<coord_t>(1, size.x() / GRID + 1);
m_cell_h = std::max<coord_t>(1, size.y() / GRID + 1);
m_grid.assign(GRID * GRID, {});
for (size_t i = 0; i < m_bboxes.size(); ++i)
for_cells(m_bboxes[i], [&](int cell) { m_grid[cell].emplace_back(i); });
}
void find(const ExPolygon &piece, std::vector<size_t> &out) const
{
out.clear();
const BoundingBox bbox = get_extents(piece);
if (!m_extent.defined || !m_extent.overlap(bbox))
return;
for_cells(bbox, [&](int cell) {
for (size_t i : m_grid[cell])
if (m_bboxes[i].overlap(bbox))
out.emplace_back(i);
});
sort_remove_duplicates(out);
if (out.size() > 1)
out.erase(std::remove_if(out.begin(), out.end(), [&](size_t i) {
const BoundingBox common(m_bboxes[i].min.cwiseMax(bbox.min), m_bboxes[i].max.cwiseMin(bbox.max));
return intersection(ClipperUtils::clip_clipper_polygons_with_subject_bbox(piece, common.inflated(SCALED_EPSILON)),
ClipperUtils::clip_clipper_polygons_with_subject_bbox(m_islands[i], common.inflated(SCALED_EPSILON))).empty();
}), out.end());
}
private:
static constexpr int GRID = 64;
template<typename Fn> void for_cells(const BoundingBox &bb, Fn &&fn) const
{
const int x0 = std::clamp(int((bb.min.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1), x1 = std::clamp(int((bb.max.x() - m_extent.min.x()) / m_cell_w), 0, GRID - 1);
const int y0 = std::clamp(int((bb.min.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1), y1 = std::clamp(int((bb.max.y() - m_extent.min.y()) / m_cell_h), 0, GRID - 1);
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
fn(y * GRID + x);
}
const ExPolygons &m_islands;
std::vector<BoundingBox> m_bboxes;
BoundingBox m_extent;
coord_t m_cell_w = 1, m_cell_h = 1;
std::vector<std::vector<size_t>> m_grid;
};
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<ExPolygons> &input_expolygons,
const std::vector<std::vector<ExPolygons>> &segmented_regions,
std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers,
const size_t num_facets_states,
const std::function<void()> &throw_on_cancel_callback)
@@ -1899,33 +2001,91 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&segmented_regions, &top_and_bottom_layers, &segmented_regions_merged, &num_facets_states, &throw_on_cancel_callback](const tbb::blocked_range<size_t> &range) {
// Every region of a layer is merged together with the regions of the islands it overlaps, and the islands are further
// apart than the dimple removal below reaches, so this gives the same result as merging the layer at once. On a layer
// cut through a fine relief every region shares thousands of hole contours with every other, and Clipper, splitting
// and re-linking one huge polygon over and over, took anything up to half an hour for a layer; per island each operation
// stays the size of the island, and the islands run in parallel.
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states);
// Zero is skipped because it is the default color of the volume
throw_on_cancel_callback();
// Group the islands joined by a region overlapping several of them; the last group takes the regions lying
// outside every island.
const ExPolygons &islands = input_expolygons[layer_idx];
const IslandLocator locator(islands);
std::vector<size_t> parent(islands.size() + 1);
std::iota(parent.begin(), parent.end(), 0);
const auto root = [&parent](size_t i) {
while (parent[i] != i)
i = parent[i] = parent[parent[i]];
return i;
};
// Islands of every piece: side regions of colours 1.., then top/bottom regions of colours 0..
std::vector<const ExPolygon *> pieces;
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
pieces.emplace_back(&piece);
if (!top_and_bottom_layers.empty())
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
pieces.emplace_back(&piece);
std::vector<std::vector<size_t>> overlapped(pieces.size());
tbb::parallel_for(size_t(0), pieces.size(), [&](size_t i) { locator.find(*pieces[i], overlapped[i]); });
std::vector<size_t> piece_island(pieces.size());
for (size_t i = 0; i < pieces.size(); ++i) {
piece_island[i] = overlapped[i].empty() ? islands.size() : overlapped[i].front();
for (size_t island : overlapped[i])
parent[root(island)] = root(piece_island[i]);
}
std::vector<size_t> bucket_of(parent.size(), size_t(-1));
size_t num_buckets = 0;
for (size_t i = 0; i < parent.size(); ++i)
if (size_t &b = bucket_of[root(i)]; b == size_t(-1))
b = num_buckets++;
// [bucket][colour]
std::vector<std::vector<ExPolygons>> sides(num_buckets, std::vector<ExPolygons>(num_facets_states));
std::vector<std::vector<ExPolygons>> tops(num_buckets, std::vector<ExPolygons>(num_facets_states));
size_t piece_idx = 0;
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
for (const ExPolygon &piece : segmented_regions[layer_idx][extruder_id])
sides[bucket_of[root(piece_island[piece_idx++])]][extruder_id].emplace_back(piece);
if (!top_and_bottom_layers.empty())
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx)
for (const ExPolygon &piece : top_and_bottom_layers[color_idx][layer_idx])
tops[bucket_of[root(piece_island[piece_idx++])]][color_idx].emplace_back(piece);
// Side regions minus the top/bottom regions of every colour.
std::vector<std::vector<ExPolygons>> merged(num_buckets, std::vector<ExPolygons>(num_facets_states));
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
Polygons tops_all;
for (const ExPolygons &t : tops[bucket])
polygons_append(tops_all, t);
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id)
if (!sides[bucket][extruder_id].empty())
merged[bucket][extruder_id] = tops_all.empty() ? std::move(sides[bucket][extruder_id]) :
diff_ex_by_piece(sides[bucket][extruder_id], tops_all);
});
// Then this colour's top/bottom regions, with the dimples removed (#7235) when the layer has side regions left.
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
throw_on_cancel_callback();
if (!segmented_regions[layer_idx][extruder_id].empty()) {
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id];
if (!top_and_bottom_layers.empty()) {
for (const std::vector<ExPolygons> &top_and_bottom_by_extruder : top_and_bottom_layers) {
if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) {
segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]);
}
}
}
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
}
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
if (!was_top_and_bottom_empty)
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) {
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
continue;
}
bool was_top_and_bottom_empty = true;
for (size_t bucket = 0; bucket < num_buckets && was_top_and_bottom_empty; ++bucket)
was_top_and_bottom_empty = merged[bucket][extruder_id].empty();
tbb::parallel_for(size_t(0), num_buckets, [&](size_t bucket) {
ExPolygons &region = merged[bucket][extruder_id];
append(region, tops[bucket][extruder_id]);
if (!was_top_and_bottom_empty && !region.empty())
region = offset2_ex(union_ex(region), float(SCALED_EPSILON), -float(SCALED_EPSILON));
});
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
}
}
}); // end of parallel_for
@@ -2213,16 +2373,56 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
assert(!color_poly.empty());
assert(!color_poly.front().empty());
if (has_layer_only_one_color(color_poly)) {
// If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer.
segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx];
} else {
MMU_Graph graph = build_graph(layer_idx, color_poly);
remove_multiple_edges_in_vertices(graph, color_poly);
graph.remove_nodes_with_one_arc();
segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states);
//segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx);
// Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to
// that island's contours than to any other island's - the way out crosses its own boundary first - so its
// Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief
// has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions
// that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in
// parallel; an island in a single colour needs no diagram at all.
const ExPolygons &islands = input_expolygons[layer_idx];
std::vector<std::pair<size_t, size_t>> island_contours(islands.size()); // [first, last) into color_poly
{
// The same order EdgeGrid::Grid::create() lists the contours in, and so colorize_contours().
size_t idx = 0;
for (size_t island_idx = 0; island_idx < islands.size(); ++island_idx) {
const size_t first = idx;
if (!islands[island_idx].contour.empty())
++idx;
for (const Polygon &hole : islands[island_idx].holes)
if (!hole.empty())
++idx;
island_contours[island_idx] = {first, idx};
}
assert(idx == color_poly.size());
}
std::vector<std::vector<ExPolygons>> island_regions(islands.size());
tbb::parallel_for(size_t(0), islands.size(), [&](size_t island_idx) {
const auto [first, last] = island_contours[island_idx];
if (first == last)
return;
const std::vector<ColoredLines> island_poly(color_poly.begin() + first, color_poly.begin() + last);
std::vector<ExPolygons> &regions = island_regions[island_idx];
if (has_layer_only_one_color(island_poly)) {
regions.assign(num_facets_states, ExPolygons());
regions[size_t(island_poly.front().front().color)].emplace_back(islands[island_idx]);
} else {
MMU_Graph graph = build_graph(layer_idx, island_poly);
remove_multiple_edges_in_vertices(graph, island_poly);
graph.remove_nodes_with_one_arc();
regions = extract_colored_segments(graph, num_facets_states);
// The faces of one colour tile it without overlapping; merged here, where an island is small,
// every later boolean gets a few regions instead of thousands of faces sharing their edges. An
// island with many holes keeps its faces: merged, each colour would be one region with thousands
// of holes, and subtracting from that is far slower than from the faces one at a time.
if (island_poly.size() <= 64)
for (ExPolygons &faces : regions)
if (faces.size() > 1)
faces = union_ex(faces);
}
});
for (std::vector<ExPolygons> &regions : island_regions)
for (size_t color_idx = 0; color_idx < regions.size(); ++color_idx)
append(segmented_regions[layer_idx][color_idx], std::move(regions[color_idx]));
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]);
@@ -2245,7 +2445,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
throw_on_cancel_callback();
}
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(input_expolygons, segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
throw_on_cancel_callback();
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
+6 -2
View File
@@ -23,11 +23,15 @@ public:
MultiPoint() {}
MultiPoint(const MultiPoint &other) : points(other.points) {}
MultiPoint(MultiPoint &&other) : points(std::move(other.points)) {}
MultiPoint(MultiPoint &&other) noexcept : points(std::move(other.points)) {}
MultiPoint(std::initializer_list<Point> list) : points(list) {}
explicit MultiPoint(const Points &_points) : points(_points) {}
// Without it, the derived classes' move constructors passing std::move(points) here copied them, which
// also means a moved-from Polygon or Polyline is now really empty where it used to silently keep its
// points: a use-after-move anywhere in the tree that happened to work before now sees nothing.
explicit MultiPoint(Points &&_points) noexcept : points(std::move(_points)) {}
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
MultiPoint& operator=(MultiPoint &&other) noexcept { points = std::move(other.points); return *this; }
virtual ~MultiPoint() = default;
void scale(double factor);
void scale(double factor_x, double factor_y);
+424 -399
View File
@@ -32,6 +32,8 @@
#include <tuple>
#include <unordered_set>
#include <thread>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <vector>
#include "libslic3r.h"
#include <utility>
@@ -2552,467 +2554,490 @@ void PerimeterGenerator::process_arachne()
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
// we need to process each island separately because we might have different
// extra perimeters for each one
for (const Surface& surface : all_surfaces) {
coord_t bead_width_0 = ext_perimeter_spacing;
// detect how many perimeters must be generated for this island
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
// Each island is independent up to its outputs, so they are generated in parallel - a layer split into thousands
// of islands (e.g. by colour painting) otherwise ran on one thread - and the outputs are then committed in the
// original island order, which is what the extra overhang perimeters (applied to the last island's loops and to
// all fill surfaces so far) depend on.
struct ArachneSurfaceResult
{
ExtrusionEntityCollection loops;
bool has_loops = false;
ExPolygons infill;
ExPolygons no_overlap;
};
std::vector<ArachneSurfaceResult> results(all_surfaces.size());
tbb::parallel_for(tbb::blocked_range<size_t>(0, all_surfaces.size()), [&](const tbb::blocked_range<size_t> &range) {
for (size_t surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) {
const Surface &surface = all_surfaces[surface_idx];
ArachneSurfaceResult &result = results[surface_idx];
coord_t bead_width_0 = ext_perimeter_spacing;
// detect how many perimeters must be generated for this island
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
coord_t wall_0_inset = 0;
if (apply_precise_outer_wall)
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
coord_t wall_0_inset = 0;
if (apply_precise_outer_wall)
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
// Set one perimeter when TopSurfaces is selected.
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
Arachne::WallToolPathsParams input_params_tmp = input_params;
Arachne::WallToolPathsParams input_params_tmp = input_params;
Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
wall_0_inset, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
wall_0_inset, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
// Check if there are some remaining perimeters to generate (the number of perimeters
// is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Check if there are some remaining perimeters to generate (the number of perimeters
// is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
coord_t perimeter_width = this->perimeter_flow.scaled_width();
coord_t perimeter_width = this->perimeter_flow.scaled_width();
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
// contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box.
BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON);
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
// contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box.
BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON);
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty();
if (top.empty())
return top;
ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty();
if (top.empty())
return top;
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons.
top = diff_ex(top, current_slices_bridges);
// Remove bridges from top surface polygons.
top = diff_ex(top, current_slices_bridges);
}
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
// due to thin lines being generated
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
return intersection_ex(top, contour);
};
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
std::vector<Arachne::VariableWidthLines> full_perimeters;
Polygons full_inner_contour;
bool full_perimeters_generated = false;
auto generate_full_perimeters = [&]() {
if (full_perimeters_generated)
return;
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
full_perimeters = full_tool_paths.getToolPaths();
full_inner_contour = full_tool_paths.getInnerContour();
full_perimeters_generated = true;
};
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
// the width added, and only beads widened by more than twice the tolerance are looked for.
const coord_t outer_wall_tolerance = bead_width_0 / 10;
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
bool nominal_uncovered = false;
// Grown by an outer wall width to take in the outer walls bordering the top surface.
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
if (nominal_uncovered) {
generate_full_perimeters();
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
perimeters = { full_perimeters.front() };
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
}
}
}
// Shrink the polygon to remove the small areas, then expand it back out plus a maragin to hide the wall line a little.
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount,
// not the full perimeter width as PS does, to enable thin lettering to print on the top surface without nozzle collisions
// due to thin lines being generated
top = offset2_ex(top, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
bool uncovered = false;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
return intersection_ex(top, contour);
};
if (uncovered) {
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
// Walls with the full count, as generated when the single perimeter feature is disabled. Generated on first use.
std::vector<Arachne::VariableWidthLines> full_perimeters;
Polygons full_inner_contour;
bool full_perimeters_generated = false;
auto generate_full_perimeters = [&]() {
if (full_perimeters_generated)
return;
Arachne::WallToolPaths full_tool_paths(last_p, bead_width_0, perimeter_spacing, coord_t(inner_loop_number + 2), wall_0_inset, layer_height, input_params_tmp);
full_perimeters = full_tool_paths.getToolPaths();
full_inner_contour = full_tool_paths.getInnerContour();
full_perimeters_generated = true;
};
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// ORCA: the single wall pass allows Arachne 2 beads across a wall, so it fills a wall narrower than 3 outer wall
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent;
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead.
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half
// the width added, and only beads widened by more than twice the tolerance are looked for.
const coord_t outer_wall_tolerance = bead_width_0 / 10;
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) {
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls.
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset));
bool nominal_uncovered = false;
// Grown by an outer wall width to take in the outer walls bordering the top surface.
const ExPolygons top_zone = offset_ex(get_top_expolygons(nominal_infill_contour, nominal_uncovered), float(bead_width_0));
if (nominal_uncovered) {
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
// feature is disabled.
generate_full_perimeters();
if (! full_perimeters.empty() && ! full_perimeters.front().empty() &&
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) {
perimeters = { full_perimeters.front() };
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
}
perimeters = std::move(full_perimeters);
infill_contour = union_ex(full_inner_contour);
}
}
//PS
bool uncovered = false;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
loop_number = int(perimeters.size()) - 1;
if (uncovered) {
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
// feature is disabled.
generate_full_perimeters();
perimeters = std::move(full_perimeters);
infill_contour = union_ex(full_inner_contour);
#ifdef ARACHNE_DEBUG
{
static int iRun = 0;
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
}
}
//PS
#endif
loop_number = int(perimeters.size()) - 1;
// All closed ExtrusionLine should have the same the first and the last point.
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
// equal the first and the last point.
assert([&perimeters = std::as_const(perimeters)]() -> bool {
for (const Arachne::VariableWidthLines& perimeter : perimeters)
for (const Arachne::ExtrusionLine& el : perimeter)
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
return false;
return true;
}());
#ifdef ARACHNE_DEBUG
{
static int iRun = 0;
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
}
#endif
int start_perimeter = int(perimeters.size()) - 1;
int end_perimeter = -1;
int direction = -1;
// All closed ExtrusionLine should have the same the first and the last point.
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
// equal the first and the last point.
assert([&perimeters = std::as_const(perimeters)]() -> bool {
for (const Arachne::VariableWidthLines& perimeter : perimeters)
for (const Arachne::ExtrusionLine& el : perimeter)
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
return false;
return true;
}());
int start_perimeter = int(perimeters.size()) - 1;
int end_perimeter = -1;
int direction = -1;
bool is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner ||
this->config->wall_sequence == WallSequence::InnerOuterInner;
bool is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner ||
this->config->wall_sequence == WallSequence::InnerOuterInner;
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner;
}
if (is_outer_wall_first) {
start_perimeter = 0;
end_perimeter = int(perimeters.size());
direction = 1;
}
std::vector<Arachne::ExtrusionLine*> all_extrusions;
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
if (perimeters[perimeter_idx].empty())
continue;
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
all_extrusions.emplace_back(&wall);
}
// Find topological order with constraints from extrusions_constrains.
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
for (auto [before, after] : extrusions_constrains) {
auto after_it = map_extrusion_to_idx.find(after);
++blocked[after_it->second];
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
}
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
ordered_extrusions.reserve(all_extrusions.size());
while (ordered_extrusions.size() < all_extrusions.size()) {
size_t best_candidate = 0;
double best_distance_sqr = std::numeric_limits<double>::max();
bool is_best_closed = false;
std::vector<size_t> available_candidates;
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
if (processed[candidate] || blocked[candidate])
continue; // Not a valid candidate.
available_candidates.push_back(candidate);
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner;
}
if (is_outer_wall_first) {
start_perimeter = 0;
end_perimeter = int(perimeters.size());
direction = 1;
}
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
});
for (const size_t candidate_path_idx : available_candidates) {
auto& path = all_extrusions[candidate_path_idx];
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
if (best_distance_sqr == std::numeric_limits<double>::max()) {
best_candidate = candidate_path_idx;
is_best_closed = path->is_closed;
}
std::vector<Arachne::ExtrusionLine*> all_extrusions;
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
if (perimeters[perimeter_idx].empty())
continue;
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
all_extrusions.emplace_back(&wall);
}
// Find topological order with constraints from extrusions_constrains.
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
for (auto [before, after] : extrusions_constrains) {
auto after_it = map_extrusion_to_idx.find(after);
++blocked[after_it->second];
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
}
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
ordered_extrusions.reserve(all_extrusions.size());
while (ordered_extrusions.size() < all_extrusions.size()) {
size_t best_candidate = 0;
double best_distance_sqr = std::numeric_limits<double>::max();
bool is_best_closed = false;
std::vector<size_t> available_candidates;
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
if (processed[candidate] || blocked[candidate])
continue; // Not a valid candidate.
available_candidates.push_back(candidate);
}
const Point candidate_position = path->junctions.front().p;
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
best_candidate = candidate_path_idx;
best_distance_sqr = distance_sqr;
is_best_closed = path->is_closed;
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
});
for (const size_t candidate_path_idx : available_candidates) {
auto& path = all_extrusions[candidate_path_idx];
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
if (best_distance_sqr == std::numeric_limits<double>::max()) {
best_candidate = candidate_path_idx;
is_best_closed = path->is_closed;
}
continue;
}
const Point candidate_position = path->junctions.front().p;
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
best_candidate = candidate_path_idx;
best_distance_sqr = distance_sqr;
is_best_closed = path->is_closed;
}
}
}
auto& best_path = all_extrusions[best_candidate];
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
processed[best_candidate] = true;
for (size_t unlocked_idx : blocking[best_candidate])
blocked[unlocked_idx]--;
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
if (best_path->is_closed)
current_position = best_path->junctions[0].p; //We end where we started.
else
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
}
}
auto& best_path = all_extrusions[best_candidate];
ordered_extrusions.push_back({ best_path, best_path->is_contour() });
processed[best_candidate] = true;
for (size_t unlocked_idx : blocking[best_candidate])
blocked[unlocked_idx]--;
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
// for OI mode that is used the basis for IOI
bringContoursToFront(ordered_extrusions);
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
// Debug statement to print spacing values:
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
if (best_path->is_closed)
current_position = best_path->junctions[0].p; //We end where we started.
else
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
}
}
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
coord_t threshold_external = (apply_precise_outer_wall)
// Precise outer wall: use the full external spacing
? ( this->ext_perimeter_flow.scaled_spacing()
+ this->perimeter_flow.scaled_spacing()/2.0 )
// Normal: half ext spacing plus half int spacing
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
+ this->perimeter_flow.scaled_spacing()/2.0 );
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
// for OI mode that is used the basis for IOI
bringContoursToFront(ordered_extrusions);
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
// Debug statement to print spacing values:
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
coord_t threshold_external = (apply_precise_outer_wall)
// Precise outer wall ⇒ use “full external spacing”
? ( this->ext_perimeter_flow.scaled_spacing()
+ this->perimeter_flow.scaled_spacing()/2.0 )
// Normal ⇒ half ext spacing + half int spacing
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
+ this->perimeter_flow.scaled_spacing()/2.0 );
// Re-order extrusions based on distance
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
// Re-order extrusions based on distance
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
// We then advance the position index to move to the second island and continue until there are no more
// perimeters left.
while (position < reordered_extrusions.size()) {
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
// run through the walls to get the index values that need re-ordering until the first one for each
// is found. Start at "position" index to enable the for loop to iterate for multiple external
// perimeters in a single island
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
case 0: // external perimeter
if (outer == -1)
outer = arr_i;
break;
case 1: // first internal wall
if (first_internal==-1 && arr_i>outer && outer!=-1){
first_internal = arr_i;
}
break;
case 2: // second internal wall
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
second_internal = arr_i;
}
break;
}
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
// This means we entered a new island.
arr_i=arr_i-1; //step back one perimeter
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
break; // exit the for loop
}
}
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
if(arr_j >= second_internal){
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
current_perimeter++;
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
// We then advance the position index to move to the second island and continue until there are no more
// perimeters left.
while (position < reordered_extrusions.size()) {
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
// run through the walls to get the index values that need re-ordering until the first one for each
// is found. Start at "position" index to enable the for loop to iterate for multiple external
// perimeters in a single island
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
case 0: // external perimeter
if (outer == -1)
outer = arr_i;
break;
case 1: // first internal wall
if (first_internal==-1 && arr_i>outer && outer!=-1){
first_internal = arr_i;
}
break;
case 2: // second internal wall
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
second_internal = arr_i;
}
break;
}
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
// This means we entered a new island.
arr_i=arr_i-1; //step back one perimeter
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
break; // exit the for loop
}
}
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
if(arr_j >= second_internal){
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
current_perimeter++;
}
}
for (arr_j = position; arr_j < second_internal; ++arr_j){ // go outside in and map the remaining perimeters (external and first internal wall(s)) using the outside in wall order
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1);
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j];
}
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
}
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array
ordered_extrusions[arr_j] = inner_outer_extrusions[arr_j-position];
// go to the next perimeter from the current position to continue scanning for external walls in the same island
position = arr_i + 1;
}
// go to the next perimeter from the current position to continue scanning for external walls in the same island
position = arr_i + 1;
}
}
}
bool steep_overhang_contour = false;
bool steep_overhang_hole = false;
if (!config->overhang_reverse) {
// Skip steep overhang detection no reverse is specified
steep_overhang_contour = true;
steep_overhang_hole = true;
}
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
if (config->overhang_reverse) {
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
bool steep_overhang_contour = false;
bool steep_overhang_hole = false;
if (!config->overhang_reverse) {
// Skip steep overhang detection no reverse is specified
steep_overhang_contour = true;
steep_overhang_hole = true;
}
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
if (config->overhang_reverse) {
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
}
defer_unsupported_loops(*this, extrusion_coll);
result.loops = std::move(extrusion_coll);
result.has_loops = true;
}
defer_unsupported_loops(*this, extrusion_coll);
this->loops->append(extrusion_coll);
}
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out.
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out.
// create one more offset to be used as boundary for fill
// we offset by half the perimeter spacing (to get to the actual infill boundary)
// and then we offset back and forth by half the infill spacing to only consider the
// non-collapsing regions
coord_t inset =
(loop_number < 0) ? 0 :
(loop_number == 0) ?
// one loop
ext_perimeter_spacing :
// two or more loops?
perimeter_spacing;
coord_t top_inset = inset;
// create one more offset to be used as boundary for fill
// we offset by half the perimeter spacing (to get to the actual infill boundary)
// and then we offset back and forth by half the infill spacing to only consider the
// non-collapsing regions
coord_t inset =
(loop_number < 0) ? 0 :
(loop_number == 0) ?
// one loop
ext_perimeter_spacing :
// two or more loops?
perimeter_spacing;
coord_t top_inset = inset;
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
if(is_topmost_layer || is_bottom_layer)
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
else
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
if(is_topmost_layer || is_bottom_layer)
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
else
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
// simplify infill contours according to resolution
Polygons pp;
for (ExPolygon& ex : infill_contour)
ex.simplify_p(m_scaled_resolution, &pp);
ExPolygons not_filled_exp = union_ex(pp);
// collapse too narrow infill areas
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
// simplify infill contours according to resolution
Polygons pp;
for (ExPolygon& ex : infill_contour)
ex.simplify_p(m_scaled_resolution, &pp);
ExPolygons not_filled_exp = union_ex(pp);
// collapse too narrow infill areas
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
ExPolygons infill_exp = offset2_ex(
not_filled_exp,
float(-min_perimeter_infill_spacing / 2.),
float(inset + min_perimeter_infill_spacing / 2.));
// append infill areas to fill_surfaces
if (!top_expolygons.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
}
this->fill_surfaces->append(infill_exp, stInternal);
apply_extra_perimeters(infill_exp);
// BBS: get the no-overlap infill expolygons
{
ExPolygons polyWithoutOverlap;
polyWithoutOverlap = offset2_ex(
ExPolygons infill_exp = offset2_ex(
not_filled_exp,
float(-min_perimeter_infill_spacing / 2.),
float(+min_perimeter_infill_spacing / 2.));
if (!top_expolygons.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
}
float(inset + min_perimeter_infill_spacing / 2.));
// append infill areas to fill_surfaces
if (!top_expolygons.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_expolygons, double(top_inset)));
}
result.infill = std::move(infill_exp);
// BBS: get the no-overlap infill expolygons
{
ExPolygons polyWithoutOverlap;
polyWithoutOverlap = offset2_ex(
not_filled_exp,
float(-min_perimeter_infill_spacing / 2.),
float(+min_perimeter_infill_spacing / 2.));
if (!top_expolygons.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
result.no_overlap = std::move(polyWithoutOverlap);
}
}
});
for (ArachneSurfaceResult &result : results) {
if (result.has_loops)
// Moved, not copied: append(const ExtrusionEntity &) clones the whole wall tree of the island.
this->loops->append(std::move(result.loops));
this->fill_surfaces->append(result.infill, stInternal);
apply_extra_perimeters(result.infill);
append(*this->fill_no_overlap, std::move(result.no_overlap));
}
}
+2 -2
View File
@@ -34,7 +34,7 @@ public:
explicit Polygon(const Points &points) : MultiPoint(points) {}
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
Polygon(const Polygon &other) : MultiPoint(other.points) {}
Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {}
Polygon(Polygon &&other) noexcept : MultiPoint(std::move(other.points)) {}
static Polygon new_scale(const std::vector<Vec2d> &points) {
Polygon pgn;
pgn.points.reserve(points.size());
@@ -43,7 +43,7 @@ public:
return pgn;
}
Polygon& operator=(const Polygon &other) { points = other.points; return *this; }
Polygon& operator=(Polygon &&other) { points = std::move(other.points); return *this; }
Polygon& operator=(Polygon &&other) noexcept { points = std::move(other.points); return *this; }
Point& operator[](Points::size_type idx) { return this->points[idx]; }
const Point& operator[](Points::size_type idx) const { return this->points[idx]; }
+2 -2
View File
@@ -26,7 +26,7 @@ class Polyline : public MultiPoint {
public:
Polyline() {};
Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {}
Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
Polyline(Polyline &&other) noexcept : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
Polyline(std::initializer_list<Point> list) : MultiPoint(list) {
fitting_result.clear();
}
@@ -47,7 +47,7 @@ public:
fitting_result = other.fitting_result;
return *this;
}
Polyline& operator=(Polyline&& other) {
Polyline& operator=(Polyline&& other) noexcept {
points = std::move(other.points);
fitting_result = std::move(other.fitting_result);
return *this;
+3
View File
@@ -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",
+25
View File
@@ -5340,6 +5340,31 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
}
}
void Print::reload_gcode_moves(GCodeProcessorResult* result) const
{
GCodeProcessor processor;
GCodeProcessor::s_IsBBLPrinter = is_BBL_printer();
const Vec3d origin = this->get_plate_origin();
processor.set_xy_offset(origin(0), origin(1));
// Estimate the per-move times with the same nozzle-grouping slot context as the export.
if (result->nozzle_group_result)
processor.initialize_from_context(result->nozzle_group_result);
try {
processor.process_file(result->filename);
} catch (const std::exception& ex) {
// The edited file is what gets printed, so failing to preview it must not fail the slice.
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": cannot re-read the G-code file " << result->filename << ": " << ex.what();
std::lock_guard<std::mutex> lock(result->result_mutex);
result->lines_ends.clear();
return;
}
GCodeProcessorResult& reloaded = processor.result();
std::lock_guard<std::mutex> lock(result->result_mutex);
result->moves = std::move(reloaded.moves);
result->lines_ends = std::move(reloaded.lines_ends);
}
std::tuple<float, float> Print::object_skirt_offset(double margin_height) const
{
if (config().skirt_loops == 0 || config().skirt_type != stPerObject || m_objects.empty())
+9
View File
@@ -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
+87
View File
@@ -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
+27
View File
@@ -172,6 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
// Orca: Infill patterns laid out by an octree, which each connected body always gets of its own.
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))
+389 -184
View File
@@ -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"
@@ -47,6 +48,7 @@
#include <cstdlib>
#include <cstdint>
#include <float.h>
#include <array>
#include <functional>
#include <ios>
#include <iomanip>
@@ -73,6 +75,7 @@
#include <Eigen/Core>
#include <tbb/parallel_for.h>
#include <tbb/parallel_invoke.h>
#include <tbb/spin_mutex.h>
#include <tbb/concurrent_unordered_set.h>
@@ -1297,6 +1300,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 +1735,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"
@@ -1942,7 +1980,9 @@ void PrintObject::detect_surfaces_type()
bool interface_shells = ! spiral_mode && m_config.interface_shells.value;
size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size();
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
// others, so the regions run next to each other instead of one after another, each still over all layers.
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start";
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
for (Layer *layer : m_layers)
@@ -2000,7 +2040,7 @@ void PrintObject::detect_surfaces_type()
if (upper_layer) {
ExPolygons upper_slices = interface_shells ?
diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) :
diff_ex(layerm_slices_surfaces, upper_layer->lslices, ApplySafetyOffset::Yes);
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(upper_layer->lslices), ApplySafetyOffset::Yes);
surfaces_append(top, opening_ex(upper_slices, offset), stTop);
} else {
// if no upper layer, all surfaces of this one are solid
@@ -2026,7 +2066,7 @@ void PrintObject::detect_surfaces_type()
surfaces_append(
bottom,
opening_ex(
diff_ex(layerm_slices_surfaces, lower_layer->lslices, ApplySafetyOffset::Yes),
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(lower_layer->lslices), ApplySafetyOffset::Yes),
offset),
surface_type_bottom_other);
// if user requested internal shells, we need to identify surfaces
@@ -2057,34 +2097,44 @@ void PrintObject::detect_surfaces_type()
// and top surfaces; let's do an intersection to discover them and consider them
// as bottom surfaces (to allow for bridge detection)
if (! top.empty() && ! bottom.empty()) {
const auto cracks = intersection_ex(top, bottom);
const auto cracks = intersection_ex_by_piece(to_expolygons(top), to_polygons(bottom));
if (!cracks.empty()) {
if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom
const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5;
// Only the bottom surfaces near a crack can take part: one that contains it must contain its box,
// and one whose box misses the grown crack is left unchanged by removing it. A layer cut through
// a fine relief has thousands of both, which made this loop quadratic.
for (const auto& crack : cracks) {
if (offset_ex(crack, small_crack_threshold).empty()) {
// For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well
if (std::any_of(bottom.begin(), bottom.end(), [&crack, small_crack_threshold](const Surface& s) {
const BoundingBox crack_bbox = get_extents(crack);
if (std::any_of(bottom.begin(), bottom.end(), [&crack, &crack_bbox, small_crack_threshold](const Surface& s) {
const auto& se = s.expolygon;
return diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
return get_extents(se).inflated(SCALED_EPSILON).contains(crack_bbox)
&& diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
&& se.area() > crack.area() * 2
&& !offset_ex(diff_ex(se, crack), small_crack_threshold).empty();
})) continue;
// Crack too small, leave it as part of the top surface, remove it from bottom surfaces
const ExPolygons grown_crack = offset_ex(crack, -small_crack_threshold);
const BoundingBox grown_bbox = get_extents(grown_crack);
Surfaces bot_tmp;
for (auto& b : bottom) {
surfaces_append(bot_tmp, diff_ex(b.expolygon, offset_ex(crack, -small_crack_threshold)), b.surface_type);
if (get_extents(b.expolygon).overlap(grown_bbox))
surfaces_append(bot_tmp, diff_ex(b.expolygon, grown_crack), b.surface_type);
else
bot_tmp.emplace_back(std::move(b));
}
bottom = std::move(bot_tmp);
}
}
}
Polygons top_polygons = to_polygons(std::move(top));
ExPolygons top_expolygons = to_expolygons(std::move(top));
top.clear();
surfaces_append(top, diff_ex(top_polygons, bottom), stTop);
surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop);
}
}
@@ -2175,7 +2225,7 @@ void PrintObject::detect_surfaces_type()
{
Polygons topbottom = to_polygons(top);
polygons_append(topbottom, to_polygons(bottom));
surfaces_append(surfaces_out, diff_ex(surfaces_prev_expolys, topbottom), stInternal);
surfaces_append(surfaces_out, diff_ex_by_piece(surfaces_prev_expolys, topbottom), stInternal);
}
surfaces_append(surfaces_out, std::move(top));
@@ -2352,29 +2402,31 @@ void PrintObject::detect_surfaces_type()
}
}
);
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
tbb::parallel_for( tbb::blocked_range<size_t>(0, m_layers.size()), [this, region_id](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
Surfaces &surfs = m_layers[idx_layer]->m_regions[region_id]->slices.surfaces;
for (Surface &s : surfs) {
if (s.surface_type == stInternalAfterExternalBridge) {
s.surface_type = stBottomBridge;
}
}
}
}
);
}
}
// ==============================================================================================================
// === ORCA: End of second external bridge layer changes =======================================================
// ==============================================================================================================
}); // for each this->print->region_count
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
// Once all the regions have their second bridge layer, and before their slices are trimmed into fill surfaces below.
if ((this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)) {
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_layers.size()), [this](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer)
for (LayerRegion *layerm : m_layers[idx_layer]->regions())
for (Surface &s : layerm->slices.surfaces)
if (s.surface_type == stInternalAfterExternalBridge)
s.surface_type = stBottomBridge;
});
m_print->throw_if_canceled();
}
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start";
// Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
tbb::parallel_for(
@@ -2391,7 +2443,7 @@ void PrintObject::detect_surfaces_type()
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end";
} // for each this->print->region_count
});
// Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.)
m_typed_slices = true;
@@ -2458,8 +2510,10 @@ void PrintObject::process_external_surfaces()
BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end";
}
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - start";
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - start";
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
// others, so the regions run next to each other instead of one after another, each still over all layers.
tbb::parallel_for(size_t(0), this->num_printing_regions(), [this, &surfaces_covered](size_t region_id) {
tbb::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()),
[this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) {
@@ -2474,9 +2528,9 @@ void PrintObject::process_external_surfaces()
}
}
);
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - end";
}
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - end";
}
void PrintObject::discover_vertical_shells()
@@ -2515,10 +2569,10 @@ void PrintObject::discover_vertical_shells()
// The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit.
return;
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom";
//FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1));
// One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a
// few such layers next to each other must not end up in one task.
tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, grain_size),
tbb::blocked_range<size_t>(0, num_layers, 1),
[this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) {
const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge };
const size_t num_regions = this->num_printing_regions();
@@ -2526,67 +2580,198 @@ void PrintObject::discover_vertical_shells()
m_print->throw_if_canceled();
const Layer &layer = *m_layers[idx_layer];
DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer];
// Simulate single set of perimeters over all merged regions.
float perimeter_offset = 0.f;
float perimeter_min_spacing = FLT_MAX;
const auto top_bottom_expansion = [&layer](size_t region_id) {
return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
};
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
static size_t debug_idx = 0;
++ debug_idx;
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
LayerRegion &layerm = *layer.m_regions[region_id];
float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
// Top surfaces.
append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion));
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Bottom surfaces.
append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion));
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
// First find the maxium number of perimeters per region slice.
unsigned int perimeters = 0;
for (Surface &s : layerm.slices.surfaces)
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
perimeters += layerm.region().config().wall_loops.value;
// Then calculate the infill offset.
if (perimeters > 0) {
Flow extflow = layerm.flow(frExternalPerimeter);
Flow flow = layerm.flow(frPerimeter);
perimeter_offset = std::max(perimeter_offset,
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
}
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
}
// Save some computing time by reducing the number of polygons.
cache.top_surfaces = union_(cache.top_surfaces);
cache.bottom_surfaces = union_(cache.bottom_surfaces);
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
if (perimeter_offset > 0.) {
// The layer.lslices are forced to merge by expanding them first.
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
// The top surfaces, the bottom surfaces and the holes are independent of each other.
tbb::parallel_invoke(
[&]() {
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.top_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_type(stTop), top_bottom_expansion(region_id)));
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Save some computing time by reducing the number of polygons.
cache.top_surfaces = union_(cache.top_surfaces);
},
[&]() {
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id)));
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
cache.bottom_surfaces = union_(cache.bottom_surfaces);
},
[&]() {
// Simulate single set of perimeters over all merged regions.
float perimeter_offset = 0.f;
float perimeter_min_spacing = FLT_MAX;
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
const LayerRegion &layerm = *layer.m_regions[region_id];
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
// First find the maxium number of perimeters per region slice.
unsigned int perimeters = 0;
for (const Surface &s : layerm.slices.surfaces)
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
perimeters += layerm.region().config().wall_loops.value;
// Then calculate the infill offset.
if (perimeters > 0) {
Flow extflow = layerm.flow(frExternalPerimeter);
Flow flow = layerm.flow(frPerimeter);
perimeter_offset = std::max(perimeter_offset,
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
}
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
}
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
if (perimeter_offset > 0.) {
// The layer.lslices are forced to merge by expanding them first.
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
svg.draw(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close();
}
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
svg.draw(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close();
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
}
cache.holes = union_(cache.holes);
}
cache.holes = union_(cache.holes);
});
}
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom";
}
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
// With one top/bottom cache for all regions, the shell and hole accumulation in the loop below depends on nothing
// region-specific but the shell settings and the external perimeter spacing, so a region sharing them with an earlier
// one reuses its result instead of repeating it: that accumulation is a union over several layers of top/bottom
// surfaces, and a multi-material print has a region per filament.
using AccumulationKey = std::array<double, 5>;
struct ShellAccumulation
{
AccumulationKey key;
Polygons shell;
Polygons holes;
};
const auto accumulation_key = [](const PrintRegionConfig &region_config, const LayerRegion *layerm) {
return AccumulationKey{ double(region_config.top_shell_layers.value), region_config.top_shell_thickness.value,
double(region_config.bottom_shell_layers.value), region_config.bottom_shell_thickness.value,
double(layerm->flow(frExternalPerimeter).scaled_spacing()) };
};
const auto accumulate_shell = [this, &cache_top_botom_regions](size_t idx_layer, const PrintRegionConfig &region_config,
const LayerRegion *layerm, Polygons &shell, Polygons &holes) {
const Layer *layer = m_layers[idx_layer];
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
auto combine_holes = [&holes](const Polygons &holes2) {
if (holes.empty() || holes2.empty())
holes.clear();
else
holes = intersection(holes, holes2);
};
auto combine_shells = [&shell](const Polygons &shells2) {
if (shell.empty())
shell = std::move(shells2);
else if (! shells2.empty()) {
polygons_append(shell, shells2);
// Running the union_ using the Clipper library piece by piece is cheaper
// than running the union_ all at once.
shell = union_(shell);
}
};
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
// Gather top regions projected to this layer.
coordf_t print_z = layer->print_z;
int i = int(idx_layer) + 1;
int itop = int(idx_layer) + n_top_layers;
bool at_least_one_top_projected = false;
for (; i < int(cache_top_botom_regions.size()) &&
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
++ i) {
at_least_one_top_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.top_surfaces);
}
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
// perimeter width of area
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i < int(cache_top_botom_regions.size()) &&
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
// Gather bottom regions projected to this layer.
coordf_t bottom_z = layer->bottom_z();
int i = int(idx_layer) - 1;
int ibottom = int(idx_layer) - n_bottom_layers;
bool at_least_one_bottom_projected = false;
for (; i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
-- i) {
at_least_one_bottom_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.bottom_surfaces);
}
if (!at_least_one_bottom_projected && i >= 0) {
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
};
std::vector<std::vector<ShellAccumulation>> shell_accumulations(top_bottom_surfaces_all_regions ? num_layers : 0);
if (! shell_accumulations.empty()) {
// Every (layer, key) pair is accumulated once, before the regions, so that nothing in the loop below is shared
// between them and they can run next to each other.
std::vector<std::array<size_t, 3>> todo; // layer, its slot, a region holding the key
for (size_t idx_layer = 0; idx_layer < num_layers; ++ idx_layer) {
std::vector<ShellAccumulation> &accumulations = shell_accumulations[idx_layer];
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
if (this->printing_region(region_id).config().ensure_vertical_shell_thickness.value != evstAll)
continue;
const LayerRegion *layerm = m_layers[idx_layer]->m_regions[region_id];
const AccumulationKey key = accumulation_key(layerm->region().config(), layerm);
if (std::none_of(accumulations.begin(), accumulations.end(), [&key](const ShellAccumulation &a) { return a.key == key; })) {
todo.push_back({ idx_layer, accumulations.size(), region_id });
accumulations.push_back({ key, {}, {} });
}
}
}
tbb::parallel_for(size_t(0), todo.size(), [this, &todo, &shell_accumulations, &accumulate_shell](size_t i) {
m_print->throw_if_canceled();
const LayerRegion *layerm = m_layers[todo[i][0]]->m_regions[todo[i][2]];
ShellAccumulation &out = shell_accumulations[todo[i][0]][todo[i][1]];
accumulate_shell(todo[i][0], layerm->region().config(), layerm, out.shell, out.holes);
});
m_print->throw_if_canceled();
}
const auto process_region = [&](size_t region_id) {
const PrintRegion &region = this->printing_region(region_id);
if (region.config().ensure_vertical_shell_thickness.value != evstAll )
// This region will be handled by discover_horizontal_shells().
continue;
return;
//FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1));
@@ -2626,7 +2811,7 @@ void PrintObject::discover_vertical_shells()
grain_size = 1;
tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, grain_size),
[this, region_id, &cache_top_botom_regions]
[this, region_id, &shell_accumulations, &accumulation_key, &accumulate_shell]
(const tbb::blocked_range<size_t>& range) {
// printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end());
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
@@ -2676,80 +2861,19 @@ void PrintObject::discover_vertical_shells()
}
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
polygons_append(holes, cache_top_botom_regions[idx_layer].holes);
auto combine_holes = [&holes](const Polygons &holes2) {
if (holes.empty() || holes2.empty())
holes.clear();
else
holes = intersection(holes, holes2);
};
auto combine_shells = [&shell](const Polygons &shells2) {
if (shell.empty())
shell = std::move(shells2);
else if (! shells2.empty()) {
polygons_append(shell, shells2);
// Running the union_ using the Clipper library piece by piece is cheaper
// than running the union_ all at once.
shell = union_(shell);
}
};
static constexpr const bool one_more_layer_below_top_bottom_surfaces = false;
if (int n_top_layers = region_config.top_shell_layers.value; n_top_layers > 0) {
// Gather top regions projected to this layer.
coordf_t print_z = layer->print_z;
int i = int(idx_layer) + 1;
int itop = int(idx_layer) + n_top_layers;
bool at_least_one_top_projected = false;
for (; i < int(cache_top_botom_regions.size()) &&
(i < itop || m_layers[i]->print_z - print_z < region_config.top_shell_thickness - EPSILON);
++ i) {
at_least_one_top_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.top_surfaces);
}
if (!at_least_one_top_projected && i < int(cache_top_botom_regions.size())) {
// Lets consider this a special case - with only 1 top solid and minimal shell thickness settings, the
// boundaries of solid layers are not anchored over/under perimeters, so lets fix it by adding at least one
// perimeter width of area
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].top_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i < int(cache_top_botom_regions.size()) &&
(i <= itop || m_layers[i]->bottom_z() - print_z < region_config.top_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
if (int n_bottom_layers = region_config.bottom_shell_layers.value; n_bottom_layers > 0) {
// Gather bottom regions projected to this layer.
coordf_t bottom_z = layer->bottom_z();
int i = int(idx_layer) - 1;
int ibottom = int(idx_layer) - n_bottom_layers;
bool at_least_one_bottom_projected = false;
for (; i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->bottom_z() < region_config.bottom_shell_thickness - EPSILON);
-- i) {
at_least_one_bottom_projected = true;
const DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[i];
combine_holes(cache.holes);
combine_shells(cache.bottom_surfaces);
}
if (!at_least_one_bottom_projected && i >= 0) {
Polygons anchor_area = intersection(expand(cache_top_botom_regions[idx_layer].bottom_surfaces,
layerm->flow(frExternalPerimeter).scaled_spacing()),
to_polygons(m_layers[i]->lslices));
combine_shells(anchor_area);
}
if (one_more_layer_below_top_bottom_surfaces)
if (i >= 0 &&
(i > ibottom || bottom_z - m_layers[i]->print_z < region_config.bottom_shell_thickness - EPSILON))
combine_holes(cache_top_botom_regions[i].holes);
}
const AccumulationKey key = accumulation_key(region_config, layerm);
const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr :
[&]() -> const ShellAccumulation * {
for (const ShellAccumulation &a : shell_accumulations[idx_layer])
if (a.key == key)
return &a;
return nullptr;
}();
if (reused != nullptr) {
shell = reused->shell;
holes = reused->holes;
} else
accumulate_shell(idx_layer, region_config, layerm, shell, holes);
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
@@ -2843,11 +2967,8 @@ void PrintObject::discover_vertical_shells()
Polygons object_volume;
Polygons internal_volume;
{
Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{};
Polygons shrinked_upper_slice = (idx_layer + 1) < m_layers.size() ?
to_polygons(m_layers[idx_layer + 1]->lslices) :
Polygons{};
object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice);
if (idx_layer > 0 && idx_layer + 1 < m_layers.size())
object_volume = to_polygons(intersection_ex_by_piece(m_layers[idx_layer - 1]->lslices, to_polygons(m_layers[idx_layer + 1]->lslices)));
internal_volume = closing(polygonsInternal, SCALED_EPSILON);
}
@@ -2858,15 +2979,34 @@ void PrintObject::discover_vertical_shells()
// the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy
// 2. the area does not fully cover an internal polygon
// This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small
// Both tests below compare a small piece against the whole layer. Done literally, that is
// quadratic in the number of pieces, which is what a layer split up by colour painting has,
// so each is restricted to the part of the layer near the piece with an identical result:
// object_volume is clipped to the piece's box, and only the internal polygons whose box meets
// the expanded piece take part in the count, since the others pass through the difference
// unchanged and add the same number to both sides of it.
std::vector<BoundingBox> internal_bboxes;
internal_bboxes.reserve(internal_volume.size());
for (const Polygon &poly : internal_volume)
internal_bboxes.emplace_back(get_extents(poly));
regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(),
[&internal_volume, &min_perimeter_infill_spacing,
[&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing,
&object_volume](const ExPolygon &p) {
return (p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p), object_volume).empty())) &&
diff(internal_volume,
expand(to_polygons(p), min_perimeter_infill_spacing))
.size() >= internal_volume.size();
const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p),
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
object_volume, get_extents(p).inflated(SCALED_EPSILON)))
.empty());
if (!small)
return false;
const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing);
const BoundingBox bbox = get_extents(expanded);
Polygons nearby;
for (size_t i = 0; i < internal_volume.size(); ++i)
if (internal_bboxes[i].overlap(bbox))
nearby.emplace_back(internal_volume[i]);
return diff(nearby, expanded).size() >= nearby.size();
}),
regularized_shell.end());
}
@@ -2888,8 +3028,9 @@ void PrintObject::discover_vertical_shells()
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
// Trim the internal & internalvoid by the shell.
Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell);
Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), regularized_shell);
const Polygons regularized_shell_polygons = to_polygons(regularized_shell);
Slic3r::ExPolygons new_internal = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternal)), regularized_shell_polygons);
Slic3r::ExPolygons new_internal_void = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternalVoid)), regularized_shell_polygons);
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
@@ -2916,7 +3057,15 @@ void PrintObject::discover_vertical_shells()
layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final");
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
} // for each region
}; // for each region
if (top_bottom_surfaces_all_regions)
// Nothing is shared between the regions, and a layer cut through a fine relief takes far longer than the others,
// so they run next to each other instead of one after another.
tbb::parallel_for(size_t(0), this->num_printing_regions(), process_region);
else
// Here every region fills the one top/bottom cache with its own surfaces first.
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id)
process_region(region_id);
} // void PrintObject::discover_vertical_shells()
// #define DEBUG_BRIDGE_OVER_INFILL
@@ -3207,6 +3356,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) {
@@ -3436,6 +3586,16 @@ void PrintObject::bridge_over_infill()
vertical_lines[i].b = Point{x, y_max};
}
// The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be
// hit. Leaving them out gives the same intersections without building a tree over the whole layer's
// boundary for every bridge.
const coord_t scan_x_min = bb_x.min.x();
const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing;
anchors.erase(std::remove_if(anchors.begin(), anchors.end(),
[scan_x_min, scan_x_max](const Line &l) {
return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max;
}),
anchors.end());
auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)};
auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)};
@@ -3680,26 +3840,58 @@ void PrintObject::bridge_over_infill()
std::vector<CandidateSurface> expanded_surfaces;
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
// The expanded fill boundary depends only on the bridging flow, and total_fill_area is not
// modified below, so build it once per spacing rather than once per candidate. A layer split
// into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each.
std::map<coord_t, Polylines> boundary_by_spacing;
// expansion_area is a clean, non-overlapping set, so cutting a bridge out of it only changes the
// polygons near that bridge. The rest are passed through untouched instead of being fed to Clipper
// with the whole layer again for every candidate.
// Not `near`/`far`: the Windows headers still define those as macros, and they expand to
// nothing, which turns the declaration below into an empty one.
const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &rest) {
Polygons nearby;
for (const Polygon &p : polys)
(get_extents(p).overlap(bbox) ? nearby : rest).emplace_back(p);
return nearby;
};
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
const auto &region_config = candidate.region->region().config();
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
region_config.sparse_infill_pattern == ipOctagramSpiral;
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
ExPolygons bridge_components = intersection_ex(expand(candidate.new_polys, flow.scaled_spacing()), deep_infill_area);
const Polygons expanded_polys = expand(candidate.new_polys, flow.scaled_spacing());
// deep_infill_area and internal_unsupported_area cover the whole layer; only their part under
// this candidate can change the results, so they are clipped to its box first.
ExPolygons bridge_components;
if (!expanded_polys.empty())
bridge_components = intersection_ex(expanded_polys,
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
deep_infill_area, get_extents(expanded_polys).inflated(SCALED_EPSILON)));
// Orca: Filter whole bridge areas so their holes remain holes.
bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(),
[&internal_unsupported_area](const ExPolygon &component) {
return intersection_ex(component, internal_unsupported_area).empty();
return intersection_ex(component, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
internal_unsupported_area,
get_extents(component).inflated(SCALED_EPSILON)))
.empty();
}),
bridge_components.end());
Polygons area_to_be_bridge = to_polygons(std::move(bridge_components));
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
if (area_to_be_bridge.empty())
continue;
Polylines boundary_plines = to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing()));
// Not split like the cut of expansion_area below: the whole limiting area is grown by 30% of the spacing,
// which merges neighbouring polygons, and any of its boundary can anchor the bridge.
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
auto boundary_it = boundary_by_spacing.find(flow.scaled_spacing());
if (boundary_it == boundary_by_spacing.end())
boundary_it = boundary_by_spacing
.emplace(flow.scaled_spacing(), to_polylines(expand(total_fill_area, 1.3f * flow.scaled_spacing())))
.first;
Polylines boundary_plines = boundary_it->second;
{
Polylines limiting_plines = to_polylines(expand(limiting_area, 0.3f * flow.scaled_spacing()));
boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end());
@@ -3775,9 +3967,12 @@ void PrintObject::bridge_over_infill()
// Check collision with other expanded surfaces
{
bool reconstruct = false;
Polygons tmp_expanded_area = expand(bridging_area, 3.0f * flow.scaled_spacing());
Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing());
const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area);
for (const CandidateSurface &s : expanded_surfaces) {
if (!intersection(s.new_polys, tmp_expanded_area).empty()) {
// Surfaces whose boxes miss each other cannot intersect, which is most pairs on a busy layer.
if (get_extents(s.new_polys).overlap(tmp_expanded_bbox) &&
!intersection(s.new_polys, tmp_expanded_area).empty()) {
bridging_angle = s.bridge_angle;
reconstruct = true;
break;
@@ -3801,10 +3996,20 @@ void PrintObject::bridge_over_infill()
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
bridging_angle, scan_spacing, true));
}
bridging_area = intersection(bridging_area, limiting_area);
bridging_area = intersection(bridging_area, total_fill_area);
bridging_area = diff(bridging_area, total_top_area);
expansion_area = diff(expansion_area, bridging_area);
// Each of these meets one bridge with the whole layer, so the layer side is first cut down to the
// bridge's box (and expansion_area split as above); the result is the same.
if (!bridging_area.empty()) {
const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON);
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox));
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox));
bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox));
}
if (!bridging_area.empty()) {
Polygons kept;
const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept);
append(kept, diff(cut, bridging_area));
expansion_area = std::move(kept);
}
#ifdef DEBUG_BRIDGE_OVER_INFILL
debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r),
+1 -1
View File
@@ -1004,9 +1004,9 @@ public:
::fread(&y, sizeof(coord_t), 1, file);
poly.points.emplace_back(Point(x * scale, y * scale));
}
printf("Polygon %d, area: %lf\n", i, area(poly.points));
if (which == -1 || which == i)
m_support_polygons_deserialized.emplace_back(std::move(poly));
printf("Polygon %d, area: %lf\n", i, area(poly.points));
}
::fread(&n_polygons, 4, 1, file);
m_trimming_polygons_deserialized.reserve(n_polygons);
+37 -7
View File
@@ -996,16 +996,46 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
if (is_auto(stype) && config_detect_sharp_tails)
{
// BBS detect sharp tail
// On a belt, "below" has to include the belt itself and the
// shear-advanced lower layer, or every belt-contact island reads as
// a sharp tail -- which is what the empty-predecessor skip above was
// really masking. effective_lower is exactly that notion of below.
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
// Each island is tested only against the lower islands whose box meets its own; overlaps() tries
// every pair, which is quadratic in the island counts of the two layers.
std::vector<BoundingBox> lower_bboxes;
lower_bboxes.reserve(tail_lower.size());
for (const ExPolygon &lower : tail_lower)
lower_bboxes.emplace_back(get_extents(lower));
for (const ExPolygon& expoly : curr_polys) {
bool is_sharp_tail = false;
// 1. nothing below
// this is a sharp tail region if it's floating and non-ignorable.
// On a belt, "below" has to include the belt itself and the
// shear-advanced lower layer, or every belt-contact island reads as
// a sharp tail -- which is what the empty-predecessor skip above was
// really masking. effective_lower is exactly that notion of below.
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) {
// this is a sharp tail region if it's floating and non-ignorable
const ExPolygons expanded = offset_ex(expoly, 0.1 * extrusion_width_scaled);
const BoundingBox bbox = get_extents(expanded);
ExPolygons lower_nearby;
for (size_t i = 0; i < tail_lower.size(); ++i)
if (lower_bboxes[i].overlap(bbox))
lower_nearby.emplace_back(tail_lower[i]);
// As overlaps(expanded, lower_nearby), with each lower island cut to the island's box first:
// below a fine relief the lower layer is a few islands with thousands of holes, and the whole
// of that boundary would otherwise be intersected once per island above.
const auto overlaps_nearby = [&]() {
for (const ExPolygon &a : expanded) {
if (a.empty())
continue;
const BoundingBox a_bbox = get_extents(a);
for (const ExPolygon &b : lower_nearby) {
if (b.empty() || !get_extents(b).overlap(a_bbox))
continue;
const Polygons b_near = ClipperUtils::clip_clipper_polygons_with_subject_bbox(b, a_bbox.inflated(SCALED_EPSILON));
if (!intersection_pl(to_polylines(b_near), a).empty() || b.contains(a.contour.points.front()))
return true;
}
}
return false;
};
if (!overlaps_nearby()) {
is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty();
}
+2 -2
View File
@@ -68,7 +68,7 @@ public:
thickness(other.thickness), thickness_layers(other.thickness_layers),
bridge_angle(other.bridge_angle), extra_perimeters(other.extra_perimeters)
{};
Surface(Surface &&rhs)
Surface(Surface &&rhs) noexcept
: surface_type(rhs.surface_type), expolygon(std::move(rhs.expolygon)),
thickness(rhs.thickness), thickness_layers(rhs.thickness_layers),
bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters)
@@ -94,7 +94,7 @@ public:
return *this;
}
Surface& operator=(Surface &&rhs)
Surface& operator=(Surface &&rhs) noexcept
{
surface_type = rhs.surface_type;
expolygon = std::move(rhs.expolygon);
-14
View File
@@ -247,20 +247,6 @@ void smooth_height_pixels(std::vector<uint8_t> &pixels, int width, int height, f
}
} // namespace
bool height_texture_has_color(const TextureDisplacementLayer &layer)
{
if (layer.empty())
return false;
{
std::lock_guard<std::mutex> lock(g_decoded_texture_cache.mutex);
const auto it = g_decoded_texture_cache.entries.find(layer.image_data.get());
if (it != g_decoded_texture_cache.entries.end() && it->second.first.lock() == layer.image_data)
return it->second.second.has_color();
}
// Not decoded yet. Decoding caches the raw image, so this happens once per image.
return decode_height_texture(layer).has_color();
}
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer)
{
DecodedHeightTexture result;
+5 -17
View File
@@ -1,7 +1,6 @@
#ifndef slic3r_TextureDisplacement_hpp_
#define slic3r_TextureDisplacement_hpp_
#include <cmath>
#include <cstddef>
#include <Eigen/Core>
#include <cstdint>
@@ -388,10 +387,6 @@ struct TextureDisplacementOptions
// image (TextureDetail::flat_colors): a texture of flat colours prints in single filaments, a
// photograph or gradient in mixes. Off forces single filaments everywhere.
bool color_mix_enabled = true;
// The most mixes the palette may offer. Every mix a bake paints with becomes a mixed filament slot,
// so this is also the most slots one bake can add. The mixes themselves are picked from the
// texture's colours, those that improve the match the most coming first.
int color_mix_count = 8;
// Majority-filter passes over the assigned colours. See TextureColorRequest::despeckle_passes -
// this is the control for it, and 2 is enough to clear the salt-and-pepper an image with detail
// finer than the mesh leaves behind, without eating features that are genuinely a facet wide.
@@ -401,7 +396,7 @@ struct TextureDisplacementOptions
{
ar(displace_border, smooth_enabled, smooth_strength, smooth_iterations, smooth_skip_border,
pipeline_v2, v2_refine_mm, v2_regularize, v2_max_triangles_k,
v2_relocate, color_mix_enabled, color_despeckle, color_mix_count);
v2_relocate, color_mix_enabled, color_despeckle);
}
};
@@ -485,10 +480,6 @@ struct DecodedHeightTexture
// DecodedHeightTexture if image_data is empty or is not a PNG at all.
DecodedHeightTexture decode_height_texture(const TextureDisplacementLayer &layer);
// decode_height_texture(layer).has_color(), answered from the decode cache rather than from a copy of the
// texture - cheap enough to ask every frame. Smoothing does not change it, so the raw decode is what is read.
bool height_texture_has_color(const TextureDisplacementLayer &layer);
// Maps a linear RGB colour in [0, 1] to an index into the caller's palette, or -1 for "no colour".
//
// Deliberately a callback rather than a function here: matching a colour to a filament is a
@@ -503,14 +494,12 @@ using ColorQuantizeFn = std::function<int(const Vec3f &)>;
// interleaving, which the slicer does per print layer. Plain data, so it can be captured into a job.
struct PrintableColor
{
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the colour its mixed filament slot shows
Vec3f rgb = Vec3f::Zero(); // what it looks like; for a mix, the perceptual average of the two
int a = 0; // filament index
int b = 0; // the second filament; == a for a pure entry
int num = 1; // a's share of the interleave, out of `den`
int den = 1;
bool is_mix() const { return a != b; }
// a's share in percent, the form a mixed filament slot is created from.
int a_percent() const { return int(std::lround(100.0 * double(num) / double(den))); }
};
// Everything needed to colour a mesh, captured on the main thread and handed to a job. An empty
@@ -773,10 +762,9 @@ struct TextureColorRequest
float min_color_region_mm2 = 0.5f;
// Filled per *base mesh* triangle (the bake is topology-preserving, so this indexes the returned
// mesh too): the quantize callback's index plus one, or 0 for "this triangle takes no colour from
// the texture". The +1 lines up with EnforcerBlockerType, where 0 is NONE ("use the volume's own
// filament"): where every palette entry is a filament, these go straight to a TriangleSelector. A
// palette with mixes maps each index to the mix's filament slot first (see
// GLGizmoTextureDisplacement::palette_filaments()).
// the texture". The +1 is not arbitrary - it lines up with EnforcerBlockerType, where 0 is NONE
// ("use the volume's own filament") and 1..16 are Extruder1..16, so the caller can hand these
// straight to a TriangleSelector without a second mapping table.
std::vector<uint8_t> *out_triangle = nullptr;
};
+4 -4
View File
@@ -167,10 +167,10 @@ inline void append(std::vector<T, Alloc> &dest, std::vector<T, Alloc> &&src)
{
if (dest.empty())
dest = std::move(src);
else {
dest.reserve(dest.size() + src.size());
std::move(std::begin(src), std::end(src), std::back_inserter(dest));
}
else
// insert() grows the capacity geometrically; reserving exactly the new size reallocated on every call, which
// made appending piece by piece quadratic.
dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
src.clear();
src.shrink_to_fit();
}
+95 -5
View File
@@ -149,6 +149,56 @@ std::unique_ptr<AppAction> make_action(const std::string& plugin_key, const std:
return std::make_unique<PluginScriptAction>(plugin_key, capability, source_name);
}
// 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());
+1
View File
@@ -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
+5 -4
View File
@@ -283,11 +283,12 @@ void BackgroundSlicingProcess::process_fff()
m_temp_output_path = this->get_current_plate()->get_tmp_gcode_path();
m_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");
+10 -2
View File
@@ -843,9 +843,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
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) {
+8 -2
View File
@@ -895,10 +895,16 @@ void GCodeViewer::SequentialView::GCodeWindow::render(float top, float bottom, f
auto update_lines = [this](uint64_t start_id, uint64_t end_id) {
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);
+3
View File
@@ -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},
+1 -3
View File
@@ -113,7 +113,7 @@ Vec3f GLGizmoPainterBase::get_tilt_up_direction() const
return build_plate_tilt_up_direction().cast<float>();
}
void GLGizmoPainterBase::render_triangles(const Selection& selection, const ModelVolume* skip) const
void GLGizmoPainterBase::render_triangles(const Selection& selection) const
{
auto* shader = wxGetApp().get_shader("mm_gouraud");
if (!shader)
@@ -135,8 +135,6 @@ void GLGizmoPainterBase::render_triangles(const Selection& selection, const Mode
continue;
++mesh_id;
if (mv == skip)
continue;
Transform3d trafo_matrix;
if (m_parent.get_canvas_type() == GLCanvas3D::CanvasAssembleView) {
+2 -8
View File
@@ -48,9 +48,6 @@ public:
virtual ~TriangleSelectorGUI() = default;
virtual void render(ImGuiWrapper* imgui, const Transform3d& matrix);
// The seed-fill contour alone, as render() last built it - for a gizmo that draws over the selector
// and has to put the contour back on top.
void render_paint_contour(const Transform3d& matrix);
//void render(const Transform3d& matrix) { this->render(nullptr, matrix); }
void set_wireframe_needed(bool need_wireframe) { m_need_wireframe = need_wireframe; }
bool get_wireframe_needed() { return m_need_wireframe; }
@@ -93,6 +90,7 @@ protected:
GLModel m_paint_contour;
void update_paint_contour();
void render_paint_contour(const Transform3d& matrix);
bool m_need_wireframe {false};
};
@@ -233,8 +231,7 @@ public:
bool on_mouse(const wxMouseEvent &mouse_event) override;
protected:
// Draws every model part's selector, except `skip`'s when given.
virtual void render_triangles(const Selection& selection, const ModelVolume* skip = nullptr) const;
virtual void render_triangles(const Selection& selection) const;
void render_cursor();
void render_cursor_circle();
void render_cursor_sphere(const Transform3d& trafo) const;
@@ -331,9 +328,6 @@ protected:
TriangleSelector::ClippingPlane get_clipping_plane_in_volume_coordinates(const Transform3d &trafo) const;
// True while a paint or erase stroke is under way.
bool is_painting() const { return m_button_down != Button::None; }
private:
std::vector<std::vector<ProjectedMousePosition>> get_projected_mouse_positions(const Vec2d &mouse_position, double resolution, const std::vector<Transform3d> &trafo_matrices) const;
@@ -8,7 +8,6 @@
#include "ColorSpaceConvert.hpp"
#include "libslic3r/AABBTreeIndirect.hpp"
#include "libslic3r/Color.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/MeshBoolean.hpp"
#include "libslic3r/Model.hpp"
@@ -70,7 +69,6 @@
#include <array>
#include <cmath>
#include <limits>
#include <numeric>
#include <queue>
#include <set>
#include <vector>
@@ -259,54 +257,11 @@ TriangleSelector::TriangleSplittingData remap_texture_paint_spatial(
// entry to fill.
constexpr int PALETTE_LUT_EDGE = 24;
// The shaded preview shader's palette arrays. The palette itself stays within the paint mask's
// EnforcerBlockerType::ExtruderMax states, since every entry has to become a filament.
// Ceiling on the printable palette, which bounds that fill cost (and the shader's uniform array).
constexpr int PALETTE_MAX_ENTRIES = 64;
// A mix is an interleave that only reads as its colour from a distance; up close it is stripes. So it
// is spent only where it beats the nearest single filament by this much (CIEDE2000). Two is about
// where a side-by-side difference stops being arguable; a margin of ten already turns most of a
// greyscale ramp - the shape a height texture actually traces - back into single filaments. The
// quantizer, the mix ranking and the shaded preview shader all apply it, so they agree on where a mix
// is used.
constexpr float PREFER_PURE_DE = 2.f;
// mix_targets(): the most pixels read per layer, and the histogram bins kept over all layers. Together
// they bound rank_mixes() to candidates x MIX_TARGET_BINS colour differences, the same order as filling
// the quantizer's lookup cube.
constexpr size_t MIX_TARGET_SAMPLES = size_t(1) << 20;
constexpr size_t MIX_TARGET_BINS = 256;
// rank_mixes() stops once the best remaining mix would improve the match by less than this, in
// CIEDE2000 averaged over every pixel of the colouring layers (see mix_targets()): a mix that only
// touches a few stray pixels is not worth a filament slot.
constexpr float MIN_MIX_GAIN = 0.05f;
// The project's mixed filament slots, one string each, as the palette cache compares them: anything
// that changes which of them a mix can reuse changes this.
std::vector<std::string> mixed_slot_signature(const DynamicPrintConfig &project_config)
{
std::vector<std::string> out;
const auto *is_mixed = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comps = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
const auto *gradient = project_config.option<ConfigOptionBools>("filament_mixed_gradient");
if (is_mixed == nullptr || comps == nullptr || ratios == nullptr)
return out;
for (size_t i = 0; i < is_mixed->values.size(); ++i)
if (is_mixed->values[i])
out.push_back(std::to_string(i) + ':' + (i < comps->values.size() ? comps->values[i] : std::string()) + '|' +
(i < ratios->values.size() ? ratios->values[i] : std::string()) + '|' +
(gradient != nullptr && i < gradient->values.size() && gradient->values[i] ? "g" : ""));
return out;
}
// Whether two palettes would draw and print the same.
bool same_palette(const std::vector<PrintableColor> &l, const std::vector<PrintableColor> &r)
{
return std::equal(l.begin(), l.end(), r.begin(), r.end(), [](const PrintableColor &x, const PrintableColor &y) {
return x.a == y.a && x.b == y.b && x.num == y.num && x.den == y.den && x.rgb == y.rgb;
});
}
// Ceiling on the filaments the palette's entries can refer to (the shaded preview shader's filament_rgb[]);
// mmu segmentation stops at Extruder16 anyway.
constexpr int PALETTE_MAX_FILAMENTS = 16;
// sRGB (0..1) <-> CIELAB, D65. Exactly what the preview shader's srgb_to_lab() computes, so the CPU
// quantizer, the mixed-palette entries and the per-fragment preview all match in the same space.
@@ -503,11 +458,6 @@ std::string GLGizmoTextureDisplacement::on_get_name() const
return _u8L("Texture displacement");
}
bool GLGizmoTextureDisplacement::on_is_activable() const
{
return m_parent.get_canvas_type() != GLCanvas3D::CanvasAssembleView && GLGizmoPainterBase::on_is_activable();
}
void GLGizmoTextureDisplacement::on_shutdown()
{
m_parent.toggle_model_objects_visibility(true);
@@ -515,25 +465,10 @@ void GLGizmoTextureDisplacement::on_shutdown()
m_shaded_preview_glmodel.reset();
m_paint_overlay_glmodel.reset();
m_paint_overlay_dirty = false;
m_painted_colors = PaintedColors{};
m_painted_colors_key.clear();
m_painted_colors_glmodel.reset();
m_painted_colors_drawn_key.clear();
m_painted_colors_runs.clear();
m_seed_fill_last_mesh_id = -1; // a hover from this session must not count in the next
// Any preview still in flight is superseded: raising the shared counter makes it abort at its next
// progress poll, and its completion handler then finds nothing to do.
m_preview_generation->fetch_add(1);
m_preview_job_pending = false;
// The palette caches hold the last volume's images; a closed gizmo should not keep them alive.
m_palette_cache.clear();
m_palette_quantizer = nullptr;
m_palette_pure_quantizer = nullptr;
m_palette_filaments.clear();
m_palette_images.clear();
m_mix_ranking.reset();
m_palette_slots.clear();
m_palette_changed = false;
m_uvcheck_glmodel.reset();
m_wireframe_overlay_glmodel.reset();
m_wireframe_overlay_vcount = 0;
@@ -603,15 +538,11 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// The shaded preview is different: it never actually moves geometry (it only shades), so
// its depth is identical to the overlay's *everywhere*, not just in the unpainted area - the
// depth-biased opaque overlay would win the depth test across the whole surface and hide the relief
// shading entirely. So render_triangles() leaves the textured volume out there. What is *not* skipped is
// shading entirely. So render_triangles() is skipped for it. What is *not* skipped is
// render_paint_overlay(): leaving the shading as the only paint feedback meant a stroke that
// erased paint, or added it with no texture picked, changed nothing on screen until the whole
// preview rebuilt at stroke end - and in the true-displacement view the opaque overlay is hidden
// by the raised surface for the same reason. The translucent tint covers both cases.
//
// A colour preview is the exception to both: the opaque overlay buries its colours under a flat plane
// wherever the relief does not rise, and the tint washes them green. Both are what the user steers by
// while painting, though, so they give way only between strokes.
// Coalesced shaded-preview rebuild from an in-progress UV island drag (see on_island_edited): done here, at
// most once per drawn frame, rather than synchronously in the UV canvas's mouse-move handler.
if (m_use_shaded_preview && m_shaded_preview_dirty) {
@@ -648,39 +579,24 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// Hide the real volume only when something is actually going to be drawn in its place; otherwise
// put it back. Getting this wrong leaves an invisible model, so it is decided once, here, rather
// than per branch below.
ModelVolume *mv = texture_volume();
m_parent.toggle_model_objects_visibility(true);
if ((use_shaded || use_true_preview) && mv != nullptr)
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
m_c->selection_info()->get_active_instance(), mv);
if (use_shaded || use_true_preview) {
if (ModelVolume *mv = texture_volume())
m_parent.toggle_model_objects_visibility(false, m_c->selection_info()->model_object(),
m_c->selection_info()->get_active_instance(), mv);
}
// Which colour preview is on screen decides what the highlight gives way to: the Normal mesh colours
// from every layer, the shaded one from the active layer only. Only a stroke brings it back - a fill
// tool's hover does not, so the colours stay on screen until the click. The debug view shows a captured
// stage, not a colour preview, so it keeps its highlight.
const TextureDisplacementLayer *al = active_layer();
const bool color_view = !is_painting() && m_debug_stage < 0;
const bool stack_colors = color_view && mv != nullptr && any_layer_colors(*mv);
const bool active_colors = color_view && al != nullptr && layer_shows_color(*al);
// Whether the textured volume's selector - and with it a fill tool's contour - is drawn this frame.
bool textured_selector_drawn = true;
if (use_shaded) {
render_shaded_preview_mesh();
// The shaded mesh is the textured volume alone, so the other model parts are still the selectors' to draw.
render_triangles(selection, mv);
textured_selector_drawn = false;
} else if (use_true_preview) {
render_preview_mesh();
if (show_paint_overlay) {
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-1.0f, -1.0f));
// Over a colour preview only the other model parts: render_preview_mesh() draws the textured one.
render_triangles(selection, stack_colors ? mv : nullptr);
render_triangles(selection);
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
}
textured_selector_drawn = show_paint_overlay && !stack_colors;
} else {
// render_triangles() *is* the model in a painter gizmo (it draws every model-part volume with the
// selector's colours), not an overlay on top of one - so it still has to run under a UV-check
@@ -691,10 +607,6 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
render_triangles(selection);
}
// The model's colour paint, over whichever surface was drawn, left out where that surface's preview shows
// paint of its own: every layer's for the Normal mesh, the active layer's otherwise.
const bool painted_colors_drawn = show_paint_overlay && m_debug_stage < 0 && render_painted_colors(use_true_preview);
// Every other layer's paint, in muted grey, so all layers stay visible while one of them is edited. Drawn
// before the active layer's tint so that one reads on top where the two overlap.
if (show_paint_overlay)
@@ -703,36 +615,15 @@ void GLGizmoTextureDisplacement::render_painter_gizmo()
// The translucent paint tint. Needed in the shaded view because the opaque highlight above is
// skipped there, and in the true-displacement view because the displaced surface rises *above*
// the undisplaced overlay geometry and hides it exactly where the relief is strongest - in both
// cases leaving an erase stroke with no visible effect until the next full preview rebuild. With no
// preview, the highlight is the selectors' own, except during a stroke over the colour paint: the paint
// there is not flushed yet, so the colours are still drawn over the stroke's highlight.
const bool preview_drawn = use_shaded || use_true_preview;
if (show_paint_overlay && (preview_drawn ? !active_colors : painted_colors_drawn && is_painting()))
// cases leaving an erase stroke with no visible effect until the next full preview rebuild.
if (show_paint_overlay && (use_shaded || use_true_preview))
render_paint_overlay(m_paint_overlay_glmodel);
// A fill tool's contour is drawn with the selectors, under the colour paint drawn since - which covers it
// on a steep face, where the paint's slope-scaled offset outruns the contour's fixed one. Put it back on top,
// at the depth its first draw stored, hence LEQUAL. Only where the selector was drawn this frame: drawing it
// is what rebuilds the contour. The tool test matters because the base keeps the last hovered mesh when the
// tool changes.
const bool fill_tool = m_tool_type == ToolType::SMART_FILL || m_tool_type == ToolType::BUCKET_FILL ||
(m_tool_type == ToolType::BRUSH && m_cursor_type == TriangleSelector::CursorType::POINTER);
const int textured_mesh_id = texture_volume_raycaster_index();
if (painted_colors_drawn && textured_selector_drawn && fill_tool && textured_mesh_id >= 0 &&
textured_mesh_id == m_seed_fill_last_mesh_id && size_t(textured_mesh_id) < m_triangle_selectors.size()) {
const ModelObject *mo = m_c->selection_info()->model_object();
GLint depth_func = GL_LESS;
glsafe(::glGetIntegerv(GL_DEPTH_FUNC, &depth_func));
glsafe(::glDepthFunc(GL_LEQUAL));
m_triangle_selectors[size_t(textured_mesh_id)]->render_paint_contour(
mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix());
glsafe(::glDepthFunc(GLenum(depth_func)));
}
// The UV editor's island selection, shown on the model. Polled here rather than pushed: the pane
// changes its selection in its own mouse handling, and a compare of a few ints per frame is free.
{
const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas();
const TextureDisplacementLayer *al = active_layer();
const UVEditorCanvas *uv_canvas = wxGetApp().plater()->get_uv_editor_canvas();
if (m_show_uv_editor && al != nullptr && al->projection_method == TextureProjectionMethod::LSCM &&
uv_canvas != nullptr && !m_uv_editor_unwrap.empty()) {
if (uv_canvas->selected_islands() != m_island_overlay_selection)
@@ -1604,9 +1495,10 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
shader->set_uniform("patch_center", m_shaded_patch_center);
shader->set_uniform("patch_axis", m_shaded_patch_axis);
// The printable palette, in RGB for display and in Lab for the match. Uploaded rather than matched
// on the CPU because the quantization is per fragment here. Count 0 means "no layer is colouring",
// and the shader keeps the model's own colour for every fragment.
// The filament palette the mesh's per-triangle indices refer to. Count 0 means "no layer is
// colouring", and the shader keeps the model's own colour for every fragment.
// The printable palette, in RGB for display and in Lab for the match. Uploaded rather than
// matched on the CPU because the quantization is per fragment here.
const GLTexture *color_tex = get_layer_color_texture(*layer);
const int palette_count =
(color_tex != nullptr) ? int(std::min(m_shaded_preview_palette.size(), size_t(PALETTE_MAX_ENTRIES))) : 0;
@@ -1614,17 +1506,24 @@ void GLGizmoTextureDisplacement::render_shaded_preview_mesh()
shader->set_uniform("has_color_tex", color_tex != nullptr);
// A flat-colour image is matched against single filaments only, as the bake does.
shader->set_uniform("pure_only", color_tex != nullptr && analyze_texture_detail(*layer).flat_colors);
shader->set_uniform("prefer_pure_de", PREFER_PURE_DE);
for (int i = 0; i < palette_count; ++i) {
const PaletteEntry &e = m_shaded_preview_palette[size_t(i)];
const std::string idx = "[" + std::to_string(i) + "]";
// An entry's colour is what it prints as: its filament's, or for a mix its mixed filament slot's.
shader->set_uniform(("palette_rgb" + idx).c_str(), e.rgb);
shader->set_uniform(("palette_lab" + idx).c_str(), srgb_to_lab(e.rgb));
// Only so the shader can tell a mix (a != b) from a single filament.
// How the entry prints: its filament, or for a mix the two it interleaves and in what ratio.
shader->set_uniform(("palette_a" + idx).c_str(), e.a);
shader->set_uniform(("palette_b" + idx).c_str(), e.b);
}
// The filaments those indices refer to, and the interleave the shader resolves a mix with - the
// the mix's smooth average colour. m_palette_filaments is what m_shaded_preview_palette was built from.
const int filament_count =
(palette_count > 0) ? int(std::min(m_palette_filaments.size(), size_t(PALETTE_MAX_FILAMENTS))) : 0;
shader->set_uniform("filament_count", filament_count);
for (int i = 0; i < filament_count; ++i) {
const ColorRGBA &c = m_palette_filaments[size_t(i)];
shader->set_uniform(("filament_rgb[" + std::to_string(i) + "]").c_str(), Vec3f(c.r(), c.g(), c.b()));
}
if (color_tex != nullptr) {
shader->set_uniform("color_tex", 1);
glsafe(::glActiveTexture(GL_TEXTURE1));
@@ -1718,151 +1617,6 @@ void GLGizmoTextureDisplacement::rebuild_other_paint_overlay()
m_other_paint_glmodel.set_color(ColorRGBA(0.55f, 0.58f, 0.60f, 0.35f));
}
std::vector<size_t> GLGizmoTextureDisplacement::PaintedColors::outside(const std::vector<bool> &excluded) const
{
std::vector<size_t> out;
out.reserve(source.size());
for (size_t i = 0; i < source.size(); ++i)
if (size_t(source[i]) >= excluded.size() || !excluded[size_t(source[i])])
out.push_back(i);
return out;
}
GLGizmoTextureDisplacement::PaintedColors GLGizmoTextureDisplacement::painted_colors(const TriangleMesh &mesh,
const TriangleSelector::TriangleSplittingData &paint)
{
PaintedColors out;
TriangleSelector selector(mesh);
selector.deserialize(paint, false);
for (const EnforcerBlockerType state : TriangleSelector::extract_used_facet_states(paint)) {
if (state == EnforcerBlockerType::NONE)
continue;
std::vector<int> source;
const indexed_triangle_set part = selector.get_facets_strict(state, &source);
// Every state comes back over the same vertex array, only the triangles differ.
if (out.facets.vertices.empty())
out.facets.vertices = part.vertices;
out.facets.indices.insert(out.facets.indices.end(), part.indices.begin(), part.indices.end());
out.source.insert(out.source.end(), source.begin(), source.end());
out.state.resize(out.facets.indices.size(), int(state));
}
return out;
}
void GLGizmoTextureDisplacement::rebuild_painted_colors(bool whole_stack)
{
const ModelVolume *mv = texture_volume();
const bool shown = mv != nullptr && any_layer_colors(*mv) && !mv->mmu_segmentation_facets.empty();
// The sub-triangles, keyed on what they were read from: the volume, its mesh and the paint.
std::string key;
if (shown)
key = std::to_string(mv->id().id) + ":" + std::to_string(reinterpret_cast<uintptr_t>(mv->mesh_ptr().get())) + ":" +
std::to_string(mv->mmu_segmentation_facets.timestamp());
if (key != m_painted_colors_key) {
m_painted_colors_key = std::move(key);
m_painted_colors = shown ? painted_colors(mv->mesh(), mv->mmu_segmentation_facets.get_data()) : PaintedColors{};
m_painted_colors_drawn_key.clear();
m_painted_colors_glmodel.reset();
m_painted_colors_runs.clear();
}
if (m_painted_colors.state.empty())
return;
// The part drawn, keyed on whose paint is left out and on that paint.
std::string drawn_key = m_painted_colors_key + (whole_stack ? std::string(":all") : ":" + std::to_string(m_active_layer_slot));
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers)
if (l.slot >= 0 && l.slot < int(TEXTURE_DISPLACEMENT_MAX_LAYERS))
drawn_key += "|" + std::to_string(l.slot) + "@" + std::to_string(mv->texture_displacement_facet(l.slot).timestamp());
if (drawn_key == m_painted_colors_drawn_key)
return;
m_painted_colors_drawn_key = std::move(drawn_key);
m_painted_colors_glmodel.reset();
m_painted_colors_runs.clear();
// Whole model triangles, as the facets record what they touch: a triangle the paint only partly covers
// is left to the preview.
std::vector<bool> excluded(mv->mesh().its.indices.size(), false);
for (const TextureDisplacementLayer &l : mv->texture_displacement_layers) {
if (l.slot < 0 || l.slot >= int(TEXTURE_DISPLACEMENT_MAX_LAYERS) || (!whole_stack && l.slot != m_active_layer_slot))
continue;
for (const TriangleSelector::TriangleBitStreamMapping &m : mv->texture_displacement_facet(l.slot).get_data().triangles_to_split)
if (m.triangle_idx >= 0 && size_t(m.triangle_idx) < excluded.size())
excluded[size_t(m.triangle_idx)] = true;
}
// One model, its triangles in filament order (painted_colors() groups them), drawn a range per filament.
const std::vector<size_t> kept = m_painted_colors.outside(excluded);
GLModel::Geometry init_data;
init_data.format = { GLModel::Geometry::EPrimitiveType::Triangles, GLModel::Geometry::EVertexLayout::P3 };
init_data.reserve_vertices(kept.size() * 3);
init_data.reserve_indices(kept.size() * 3);
unsigned n = 0;
for (const size_t t : kept) {
const int state = m_painted_colors.state[t];
if (m_painted_colors_runs.empty() || m_painted_colors_runs.back().first != state)
m_painted_colors_runs.push_back({ state, { size_t(n), size_t(n) } });
for (int i = 0; i < 3; ++i)
init_data.add_vertex(m_painted_colors.facets.vertices[size_t(m_painted_colors.facets.indices[t][i])]);
init_data.add_triangle(n, n + 1, n + 2);
n += 3;
m_painted_colors_runs.back().second.second = size_t(n);
}
if (!init_data.is_empty())
m_painted_colors_glmodel.init_from(std::move(init_data));
}
bool GLGizmoTextureDisplacement::render_painted_colors(bool whole_stack)
{
rebuild_painted_colors(whole_stack);
const ModelObject *mo = m_c->selection_info()->model_object();
const ModelVolume *mv = texture_volume();
GLShaderProgram *shader = wxGetApp().get_shader("mm_gouraud");
if (mo == nullptr || mv == nullptr || shader == nullptr || !m_painted_colors_glmodel.is_initialized())
return false;
const Selection &selection = m_parent.get_selection();
const Transform3d trafo_matrix = mo->instances[selection.get_instance_idx()]->get_transformation().get_matrix() * mv->get_matrix();
const Camera &camera = wxGetApp().plater()->get_camera();
const Transform3d &view_matrix = camera.get_view_matrix();
const Matrix3d normal_matrix = trafo_matrix.matrix().block(0, 0, 3, 3).inverse().transpose();
const std::vector<ColorRGBA> colors = wxGetApp().plater()->get_extruders_colors();
shader->start_using();
// Set up as render_triangles() sets it up, so the colours are lit, clipped and slope-marked exactly as the
// neutral surface they cover.
const ClippingPlaneDataWrapper clp_data = get_clipping_plane_data();
shader->set_uniform("clipping_plane", clp_data.clp_dataf);
shader->set_uniform("z_range", clp_data.z_range);
shader->set_uniform("view_model_matrix", view_matrix * trafo_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
shader->set_uniform("view_normal_matrix", Matrix3d(view_matrix.matrix().block(0, 0, 3, 3) * normal_matrix));
shader->set_uniform("volume_world_matrix", trafo_matrix);
shader->set_uniform("volume_mirrored", trafo_matrix.matrix().determinant() < 0.);
shader->set_uniform("slope.actived", m_parent.is_using_slope());
shader->set_uniform("slope.volume_world_normal_matrix", Matrix3f(normal_matrix.cast<float>()));
shader->set_uniform("slope.normal_z", float(-std::cos(Geometry::deg2rad(m_highlight_by_angle_threshold_deg))));
shader->set_uniform("slope.up_direction", get_tilt_up_direction());
shader->set_uniform("show_wireframe", false);
// A full depth unit in front of the selectors' highlight at -1 in the Normal view, the least OpenGL
// guarantees to tell apart. Depth writes off, as for the tint: the wireframe and seam overlays drawn later
// test against the real surface, and the tint drawn after this shows on top of it.
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.f, -2.f));
glsafe(::glDepthMask(GL_FALSE));
bool drawn = false;
for (const auto &[state, range] : m_painted_colors_runs)
if (state >= 1 && size_t(state) <= colors.size()) {
m_painted_colors_glmodel.set_color(adjust_color_for_rendering(colors[size_t(state - 1)]));
m_painted_colors_glmodel.render(range, shader);
drawn = true;
}
glsafe(::glDepthMask(GL_TRUE));
glsafe(::glDisable(GL_POLYGON_OFFSET_FILL));
shader->stop_using();
return drawn;
}
void GLGizmoTextureDisplacement::rebuild_paint_overlay()
{
m_paint_overlay_glmodel.reset();
@@ -1917,12 +1671,11 @@ void GLGizmoTextureDisplacement::render_paint_overlay(GLModel &overlay)
shader->set_uniform("view_model_matrix", camera.get_view_matrix() * trafo_matrix);
shader->set_uniform("projection_matrix", camera.get_projection_matrix());
// Translucent, and pulled toward the camera so it wins the depth test against the coincident
// shaded surface - and against the selectors' highlight at -1 in the Normal view, by the full depth
// unit OpenGL guarantees to tell apart. Depth writes are off: this is a tint, and letting it own the
// depth buffer would make the wireframe and seam overlays drawn after it fight with geometry that is
// not really there. Blending is already enabled by render_painter_gizmo().
// shaded surface. Depth writes are off: this is a tint, and letting it own the depth buffer would
// make the wireframe and seam overlays drawn after it fight with geometry that is not really
// there. Blending is already enabled by render_painter_gizmo().
glsafe(::glEnable(GL_POLYGON_OFFSET_FILL));
glsafe(::glPolygonOffset(-2.f, -2.f));
glsafe(::glPolygonOffset(-1.5f, -1.5f));
glsafe(::glDepthMask(GL_FALSE));
overlay.render();
glsafe(::glDepthMask(GL_TRUE));
@@ -2238,9 +1991,6 @@ void GLGizmoTextureDisplacement::rebuild_preview()
// finishes after the job queued below - and, since the counter is shared with the worker, that
// job also notices mid-run and aborts rather than computing a result nobody will use.
m_preview_generation->fetch_add(1);
// Everything rebuilt from here on reads the current palette. Cleared before any of the early returns
// below, which would otherwise leave it set and re-run this every frame.
m_palette_changed = false;
update_uv_editor();
rebuild_shaded_preview_mesh();
rebuild_paint_overlay();
@@ -2311,21 +2061,25 @@ void GLGizmoTextureDisplacement::queue_preview_job()
input.volume_to_world = texture_displacement_volume_to_world(*mv);
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
input.facets_data[size_t(i)] = mv->texture_displacement_facet(i).get_data();
// Captured here rather than read in the handler: the palette is main-thread state, and the preview
// has to be grouped against the same palette it was computed with, not whatever it is by the time
// the result lands.
// Captured here rather than read in the handler: get_extruders_colors() is main-thread state and
// the preview has to be grouped against the same palette it was computed with, not whatever is
// loaded by the time it lands.
input.color = color_settings_for(*mv);
// The result names a palette entry per triangle (index + 1), so these are the colours to draw it in:
// a single filament's own, or for a mix the colour its slot will show once a bake creates it.
std::vector<ColorRGBA> entry_colors;
entry_colors.reserve(input.color.palette.size());
for (const PrintableColor &e : input.color.palette)
entry_colors.emplace_back(e.rgb.x(), e.rgb.y(), e.rgb.z(), 1.f);
// The filament list the result's indices refer to, captured with the job rather than read back
// when it lands - loading a filament meanwhile must not recolour a preview computed against a
// different list.
// Every extruder, not the palette's physical-only list: the bake writes the filament it resolved
// to, and a mix resolves to a *mixed filament slot*, which is an extruder past the physical ones.
// Grouping against the shorter list dropped every triangle carrying such a slot out of the mesh
// entirely - the relief vanished and left only the few triangles that happened to print in a plain
// filament. The palette still has to be built from physical filaments alone (see filament_palette()),
// which is why these two are not the same list.
const std::vector<ColorRGBA> filaments = wxGetApp().plater()->get_extruders_colors();
m_preview_job_running = true;
auto &worker = wxGetApp().plater()->get_ui_job_worker();
queue_job(worker, std::make_unique<TextureDisplacementPreviewJob>(std::move(input), generation, m_preview_generation,
[this, entry_colors](TextureDisplacementPreviewResult result, uint64_t result_generation) {
[this, filaments](TextureDisplacementPreviewResult result, uint64_t result_generation) {
indexed_triangle_set its = std::move(result.mesh);
m_preview_job_running = false;
if (result_generation != m_preview_generation->load()) {
@@ -2339,11 +2093,14 @@ void GLGizmoTextureDisplacement::queue_preview_job()
} else {
m_preview_glmodel.reset();
m_preview_color_runs.clear();
if (result.triangle_color.size() == its.indices.size() && !entry_colors.empty()) {
if (result.triangle_color.size() == its.indices.size() && !filaments.empty()) {
// Group by *filament*, not by palette entry: what the bake wrote is the resolved
// filament, interleaving already applied, so this shows the real banding rather
// than the flat average the eye will turn it into.
indexed_triangle_set sorted;
sorted.vertices = its.vertices;
sorted.indices.reserve(its.indices.size());
for (int want = 0; want <= int(entry_colors.size()); ++want) {
for (int want = 0; want <= int(filaments.size()); ++want) {
const size_t first = sorted.indices.size();
for (size_t i = 0; i < its.indices.size(); ++i)
if (int(result.triangle_color[i]) == want)
@@ -2352,7 +2109,7 @@ void GLGizmoTextureDisplacement::queue_preview_job()
continue;
m_preview_color_runs.push_back(
{ { first * 3, sorted.indices.size() * 3 },
want == 0 ? GLVolume::NEUTRAL_COLOR : entry_colors[size_t(want - 1)] });
want == 0 ? GLVolume::NEUTRAL_COLOR : filaments[size_t(want - 1)] });
}
m_preview_glmodel.init_from(sorted);
} else {
@@ -3609,9 +3366,6 @@ void GLGizmoTextureDisplacement::update_from_model_object(bool first_update)
const ModelObject *mo = m_c->selection_info()->model_object();
m_triangle_selectors.clear();
// The base keeps the last mesh a fill tool hovered, and render_painter_gizmo() reads it as a hover that is
// still on: a new set of selectors has none.
m_seed_fill_last_mesh_id = -1;
std::vector<ColorRGBA> ebt_colors;
ebt_colors.push_back(GLVolume::NEUTRAL_COLOR);
@@ -4509,14 +4263,38 @@ TextureDisplacementFacetsData GLGizmoTextureDisplacement::facets_data_of(const M
return out;
}
bool GLGizmoTextureDisplacement::layer_shows_color(const TextureDisplacementLayer &layer)
{
return layer.color_enabled && !layer.empty() && height_texture_has_color(layer);
}
bool GLGizmoTextureDisplacement::any_layer_colors(const ModelVolume &mv)
{
return std::any_of(mv.texture_displacement_layers.begin(), mv.texture_displacement_layers.end(), layer_shows_color);
for (const TextureDisplacementLayer &layer : mv.texture_displacement_layers)
if (layer.color_enabled && !layer.empty() && decode_height_texture(layer).has_color())
return true;
return false;
}
void GLGizmoTextureDisplacement::bind_mixes_to_filament_slots(std::vector<PaletteEntry> &palette)
{
Sidebar *sidebar = &wxGetApp().plater()->sidebar();
if (sidebar == nullptr)
return;
for (PaletteEntry &e : palette) {
if (!e.is_mix())
continue;
// Components are 1-based in the config; the ratios are percentages summing to 100, which is the
// form create_mixed_filament_from_result() normalises from.
const int a_pct = int(std::lround(100.0 * double(e.num) / double(e.den)));
const int slot = sidebar->ensure_mixed_filament({ unsigned(e.a + 1), unsigned(e.b + 1) },
{ a_pct, 100 - a_pct });
if (slot >= 0) {
e.a = e.b = slot;
e.num = e.den = 1;
} else {
// No room for another slot. Collapse to the component that dominates the blend, which is what
// the old per-triangle path did on a surface it could not band anyway.
const int dominant = (e.num * 2 >= e.den) ? e.a : e.b;
e.a = e.b = dominant;
e.num = e.den = 1;
}
}
}
TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelVolume &mv)
@@ -4525,90 +4303,47 @@ TextureColorSettings GLGizmoTextureDisplacement::color_settings_for(const ModelV
if (!any_layer_colors(mv))
return out; // nothing is colouring: every colour path stays switched off
out.palette = cached_palette();
out.palette_pure = make_palette(m_palette_filaments, {});
out.palette_pure = make_palette(m_palette_filaments, /* mixing */ false, PALETTE_MAX_ENTRIES);
// Done here rather than in cached_palette(): this runs when a preview or a bake is queued, off a
// user action, while that one is also touched from the render path - and creating filament slots
// there would mutate the project mid-frame.
bind_mixes_to_filament_slots(out.palette);
out.despeckle_passes = mv.texture_displacement_options.color_despeckle;
return out;
}
const std::vector<GLGizmoTextureDisplacement::PaletteEntry> &GLGizmoTextureDisplacement::cached_palette()
{
// Rebuilt only when something it depends on actually changes. The shaded preview rebuilds on every
// paint stroke and the panel asks every frame, while ranking the mixes and filling the quantizer's
// lookup cube each take tens of milliseconds - paying that per stroke is the difference between
// painting that keeps up and painting that stutters.
//
// Two levels. The ranking depends only on the filaments and the images, so dragging the count, or a
// bake creating slots, re-picks from it without ranking again; the palette and its quantizers depend
// on that pick as well.
const ModelVolume *mv = texture_volume();
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
const int mix_count = mv != nullptr ? std::max(0, mv->texture_displacement_options.color_mix_count) : 0;
// Rebuilt only when the loaded filaments or the mixing setting actually change. The shaded preview
// rebuilds on every paint stroke and the subdivide preview on every slider frame, and filling the
// quantizer's lookup cube for a 64-entry palette is tens of milliseconds - paying that per stroke
// is the difference between painting that keeps up and painting that stutters.
const ModelVolume *mv = texture_volume();
const bool mixing = mv != nullptr && mv->texture_displacement_options.color_mix_enabled;
std::vector<ColorRGBA> filaments = filament_palette();
// The images themselves rather than their addresses, so a freed and reallocated image can never
// pass for the old one. Whether one has colour, and whether its colours are flat, follows from it.
std::vector<std::shared_ptr<std::vector<unsigned char>>> images;
if (mv != nullptr)
for (const TextureDisplacementLayer &layer : mv->texture_displacement_layers)
if (layer.color_enabled && !layer.empty())
images.push_back(layer.image_data);
const bool ranking_stale = filaments != m_palette_filaments || images != m_palette_images;
if (ranking_stale) {
// Every mix costs a filament slot once they are bound to one, and the mask can name only so many
// states, so the palette has to leave room beside the physical filaments it already counts.
const int cap = int(EnforcerBlockerType::ExtruderMax);
if (m_palette_cache.empty() || filaments != m_palette_filaments || mixing != m_palette_mixing ||
cap != m_palette_cap) {
m_palette_filaments = std::move(filaments);
m_palette_images = std::move(images);
m_mix_ranking.reset();
}
if (mixing && !m_mix_ranking)
m_mix_ranking = rank_mixes(m_palette_filaments, mix_targets(mv->texture_displacement_layers),
int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
// Which mixes the project can still print: those it has a fixed slot for, plus as many new ones as
// there are free slots.
const PresetBundle &bundle = *wxGetApp().preset_bundle;
const int free_slots = std::max(0, int(EnforcerBlockerType::ExtruderMax) - int(bundle.filament_presets.size()));
const std::vector<std::string> slots = mixed_slot_signature(bundle.project_config);
if (ranking_stale || m_palette_cache.empty() || mixing != m_palette_mixing || mix_count != m_palette_mix_count ||
free_slots != m_palette_free_slots || slots != m_palette_slots) {
m_palette_mixing = mixing;
m_palette_mix_count = mix_count;
m_palette_free_slots = free_slots;
m_palette_slots = slots;
std::vector<PaletteEntry> mixes;
if (mixing && m_mix_ranking)
mixes = pick_mixes(*m_mix_ranking, mix_count, free_slots, [&bundle](const PaletteEntry &e) {
return find_fixed_mixed_filament(bundle.project_config, {unsigned(e.a + 1), unsigned(e.b + 1)},
{e.a_percent(), 100 - e.a_percent()}) >= 0;
});
std::vector<PaletteEntry> palette = make_palette(m_palette_filaments, mixes);
// The previews keep what they were drawn with, so a palette that really changed under them -
// a filament or a mixed slot edited in the sidebar - has to send them round again.
m_palette_changed = m_palette_changed || (!m_palette_cache.empty() && !same_palette(palette, m_palette_cache));
m_palette_cache = std::move(palette);
m_palette_quantizer = nullptr;
m_palette_pure_quantizer = nullptr;
m_palette_mixing = mixing;
m_palette_cap = cap;
m_palette_cache = make_palette(m_palette_filaments, mixing, cap);
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
}
return m_palette_cache;
}
std::pair<ColorQuantizeFn, ColorQuantizeFn> GLGizmoTextureDisplacement::palette_quantizers()
{
cached_palette();
if (!m_palette_quantizer) {
m_palette_quantizer = make_palette_quantizer(m_palette_cache);
const bool has_mixes = m_palette_cache.size() > m_palette_filaments.size();
m_palette_pure_quantizer = has_mixes ? make_palette_quantizer(make_palette(m_palette_filaments, {})) : m_palette_quantizer;
}
return { m_palette_quantizer, m_palette_pure_quantizer };
}
std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
{
std::vector<ColorRGBA> all = wxGetApp().plater()->get_extruders_colors();
// Physical filaments only. A mix bakes into a mixed filament slot of its own, and those slots are
// extruders too - mixing them again would hand a blend components naming a virtual slot, where it
// can only name physical ones ("Mixed filament has invalid or mismatched components"). Mixed slots
// are kept after the physical ones, so filament i here is extruder i.
// Physical filaments only. The mixes this palette produces each become a mixed filament slot of
// their own (see bind_mixes_to_filament_slots()), and those slots are extruders too - so taking the
// list as it comes meant the next rebuild mixed *them* again, and handed components naming a
// virtual slot to a blend that can only name physical ones. That is what left entries reading
// "filament 1 plus nothing" and raised "Mixed filament has invalid or mismatched components".
const auto *is_mixed = wxGetApp().preset_bundle->project_config.option<ConfigOptionBools>("filament_is_mixed");
std::vector<ColorRGBA> palette;
palette.reserve(all.size());
@@ -4622,211 +4357,44 @@ std::vector<ColorRGBA> GLGizmoTextureDisplacement::filament_palette()
return palette;
}
std::vector<GLGizmoTextureDisplacement::MixTarget> GLGizmoTextureDisplacement::mix_targets(
const std::vector<TextureDisplacementLayer> &layers)
{
// Each layer's bins, weighted by their share of that layer's pixels.
std::vector<std::vector<MixTarget>> per_layer;
for (const TextureDisplacementLayer &layer : layers) {
if (!layer.color_enabled || layer.empty() || analyze_texture_detail(layer).flat_colors)
continue;
TextureDisplacementLayer raw = layer;
raw.smoothing = 0.f;
const DecodedHeightTexture tex = decode_height_texture(raw);
if (!tex.has_color())
continue;
// 16 levels per channel, each bin keeping the mean of the colours that fell in it: a coarse grid
// to gather on, without snapping every colour to a bin corner.
struct Bin
{
double r = 0., g = 0., b = 0.;
uint32_t n = 0;
};
std::vector<Bin> bins(size_t(16 * 16 * 16));
// Past the budget, one pixel from each run of `stride`, at an offset jittered by a fixed-seed
// generator. A fixed step would sample a lattice that a striped texture can line up with, so
// that only one of its colours is ever seen; jittered, stripes of any period or orientation are
// sampled in proportion, and the same image still always gives the same targets. The offset
// comes from the generator's high bits: a power-of-two LCG's low bits repeat every 2, 4, 8...
// steps, and the stride is a power of two for exactly the images large enough to need this.
const size_t npx = size_t(tex.width) * size_t(tex.height);
const size_t stride = std::max<size_t>(1, npx / MIX_TARGET_SAMPLES);
uint64_t state = 0x9E3779B97F4A7C15ull;
size_t sampled = 0;
for (size_t start = 0; start < npx; start += stride) {
state = state * 6364136223846793005ull + 1442695040888963407ull;
const size_t i = start + size_t((uint64_t(uint32_t(state >> 32)) * uint64_t(stride)) >> 32);
if (i >= npx)
break;
const uint8_t *px = &tex.rgb[i * 3];
Bin &bin = bins[size_t(px[0] >> 4) * 256 + size_t(px[1] >> 4) * 16 + size_t(px[2] >> 4)];
bin.r += px[0];
bin.g += px[1];
bin.b += px[2];
++bin.n;
++sampled;
}
std::vector<MixTarget> targets;
for (const Bin &bin : bins)
if (bin.n > 0) {
const double inv = 1. / (255. * double(bin.n));
targets.push_back({ srgb_to_lab(Vec3f(float(bin.r * inv), float(bin.g * inv), float(bin.b * inv))),
float(double(bin.n) / double(sampled)) });
}
per_layer.push_back(std::move(targets));
}
// The layers weigh the same and together 1, settled before the pruning below: what rank_mixes() sums
// over the kept bins is then a mean over every pixel of the colouring layers, with the pixels of a
// dropped bin counted as no better off.
std::vector<MixTarget> out;
for (std::vector<MixTarget> &targets : per_layer)
for (MixTarget &t : targets) {
t.weight /= float(per_layer.size());
out.push_back(t);
}
if (out.size() > MIX_TARGET_BINS) {
std::partial_sort(out.begin(), out.begin() + MIX_TARGET_BINS, out.end(),
[](const MixTarget &l, const MixTarget &r) { return l.weight > r.weight; });
out.resize(MIX_TARGET_BINS);
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::rank_mixes(
const std::vector<ColorRGBA> &filaments, const std::vector<MixTarget> &targets, int limit)
{
const int n = int(filaments.size());
if (n < 2 || targets.empty() || limit <= 0)
return {};
// Every pair at every short-cycle ratio, coloured as its slot will be.
std::vector<std::string> hex(filaments.size());
for (int i = 0; i < n; ++i)
hex[size_t(i)] = encode_color(filaments[size_t(i)]);
std::vector<PaletteEntry> candidates;
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j)
for (int den = 2; den <= 6; ++den)
for (int num = 1; num < den; ++num) {
if (std::gcd(num, den) != 1)
continue; // 2/4 is 1/2, already there
PaletteEntry e{ Vec3f::Zero(), i, j, num, den };
ColorRGB blended;
if (!decode_color(blend_color_multi({ hex[size_t(i)], hex[size_t(j)] },
{ e.a_percent(), 100 - e.a_percent() }),
blended))
continue;
e.rgb = Vec3f(blended.r(), blended.g(), blended.b());
candidates.push_back(e);
}
// How far each target is from the nearest single filament, and from every candidate.
const size_t nt = targets.size(), nc = candidates.size();
std::vector<Vec3f> filament_lab(size_t(n), Vec3f::Zero());
for (int i = 0; i < n; ++i)
filament_lab[size_t(i)] = srgb_to_lab(Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()));
const auto de = [](const Vec3f &l, const Vec3f &r) { return DeltaE00(l.x(), l.y(), l.z(), r.x(), r.y(), r.z()); };
std::vector<float> pure_d(nt, std::numeric_limits<float>::max());
for (size_t t = 0; t < nt; ++t)
for (const Vec3f &lab : filament_lab)
pure_d[t] = std::min(pure_d[t], de(targets[t].lab, lab));
std::vector<float> dist(nc * nt);
tbb::parallel_for(tbb::blocked_range<size_t>(0, nc), [&](const tbb::blocked_range<size_t> &range) {
for (size_t c = range.begin(); c < range.end(); ++c) {
const Vec3f lab = srgb_to_lab(candidates[c].rgb);
for (size_t t = 0; t < nt; ++t)
dist[c * nt + t] = de(targets[t].lab, lab);
}
});
// Greedy: each round takes the candidate that lowers the weighted error the most. A candidate only
// counts where it beats the single filament by PREFER_PURE_DE, since everywhere else the quantizer
// picks the filament anyway.
std::vector<float> current = pure_d;
std::vector<char> taken(nc, 0);
std::vector<PaletteEntry> out;
const auto counts = [&](size_t c, size_t t) {
const float d = dist[c * nt + t];
return d < current[t] && d <= pure_d[t] - PREFER_PURE_DE;
};
while (int(out.size()) < limit) {
size_t best = nc;
double best_gain = 0.;
for (size_t c = 0; c < nc; ++c) {
if (taken[c])
continue;
double gain = 0.;
for (size_t t = 0; t < nt; ++t)
if (counts(c, t))
gain += double(targets[t].weight) * double(current[t] - dist[c * nt + t]);
if (gain > best_gain) {
best_gain = gain;
best = c;
}
}
if (best == nc || best_gain < double(MIN_MIX_GAIN))
break;
taken[best] = 1;
out.push_back(candidates[best]);
for (size_t t = 0; t < nt; ++t)
if (counts(best, t))
current[t] = dist[best * nt + t];
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::pick_mixes(
const std::vector<PaletteEntry> &ranking, int count, int free_slots, const std::function<bool(const PaletteEntry &)> &reusable)
{
std::vector<PaletteEntry> out;
for (const PaletteEntry &e : ranking) {
if (int(out.size()) >= count)
break;
// Out of free slots, a later mix that already has one still fits.
if (reusable && reusable(e)) {
out.push_back(e);
} else if (free_slots > 0) {
out.push_back(e);
--free_slots;
}
}
return out;
}
std::vector<GLGizmoTextureDisplacement::PaletteEntry> GLGizmoTextureDisplacement::make_palette(
const std::vector<ColorRGBA> &filaments, const std::vector<PaletteEntry> &mixes)
const std::vector<ColorRGBA> &filaments, bool mixing, int max_entries)
{
std::vector<PaletteEntry> out;
out.reserve(filaments.size() + mixes.size());
for (int i = 0; i < int(filaments.size()); ++i)
out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()), i, i, 1, 1 });
out.insert(out.end(), mixes.begin(), mixes.end());
return out;
}
const int n = int(filaments.size());
for (int i = 0; i < n; ++i)
out.push_back({ Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()),
i, i, 1, 1 });
if (!mixing || n < 2)
return out;
std::vector<int> GLGizmoTextureDisplacement::palette_filaments(const std::vector<PaletteEntry> &palette,
const std::vector<uint8_t> &triangle_color,
const std::function<int(const PaletteEntry &)> &slot_for_mix)
{
std::vector<char> used(palette.size(), 0);
for (const uint8_t v : triangle_color)
if (v > 0 && size_t(v) <= palette.size())
used[size_t(v) - 1] = 1;
// How many intermediate steps each pair gets, chosen so the whole palette stays under
// PALETTE_MAX_ENTRIES. Fewer filaments means more room for mixes, which is also what you want:
// with two filaments the mixes are the only way to get anywhere, and with sixteen there is little
// point mixing at all. `den` is also the band/dither repeat, so a small one is a short pattern.
const int pairs = n * (n - 1) / 2;
int steps = 0;
for (int s = 5; s >= 1; --s)
if (n + pairs * s <= max_entries) {
steps = s;
break;
}
if (steps == 0)
return out;
const int den = steps + 1;
std::vector<int> out(palette.size(), -1);
for (size_t i = 0; i < palette.size(); ++i) {
const PaletteEntry &e = palette[i];
if (!e.is_mix()) {
out[i] = e.a;
} else if (used[i]) {
const int slot = slot_for_mix ? slot_for_mix(e) : -1;
// No room for another slot: the component that dominates the blend is the nearest the print
// can come.
out[i] = slot >= 0 ? slot : (e.num * 2 >= e.den ? e.a : e.b);
for (int i = 0; i < n; ++i)
for (int j = i + 1; j < n; ++j) {
const Vec3f lab_i = srgb_to_lab(Vec3f(filaments[size_t(i)].r(), filaments[size_t(i)].g(), filaments[size_t(i)].b()));
const Vec3f lab_j = srgb_to_lab(Vec3f(filaments[size_t(j)].r(), filaments[size_t(j)].g(), filaments[size_t(j)].b()));
for (int k = 1; k <= steps; ++k) {
// k/den of filament i, the rest of j - averaged in Lab, which is what the eye does
// when the two are interleaved too finely to resolve.
const float t = float(k) / float(den);
out.push_back({ lab_to_srgb(lab_i * t + lab_j * (1.f - t)), i, j, k, den });
}
}
}
return out;
}
@@ -4866,7 +4434,15 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
best_pure = int(i);
}
}
// A mix only where it clearly beats the nearest single filament, see PREFER_PURE_DE.
// A mix is an interleave that only reads as its colour from a distance; up close it is
// stripes. So it is spent only where it buys a better match than the nearest single
// filament - but "better" was set at ten Delta E, which is not a visible step, it is a
// different colour. Measured over the whole cube that threshold turned 94% of the
// lookups that wanted a mix back into a pure filament, leaving 38%; along a greyscale
// ramp, the shape a height texture actually traces, it cut 80% to 66%. Two Delta E is
// about where a side-by-side difference stops being arguable, which is the right place
// to start paying for stripes.
constexpr float PREFER_PURE_DE = 2.f;
if (best_pure >= 0 && palette[size_t(best)].is_mix() && best_pure_d - best_d < PREFER_PURE_DE)
best = best_pure;
(*lut)[(size_t(r) * E + size_t(g)) * E + size_t(b)] = uint8_t(best);
@@ -4885,7 +4461,7 @@ ColorQuantizeFn GLGizmoTextureDisplacement::make_palette_quantizer(const std::ve
TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
const indexed_triangle_set &base, const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers, const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings, const DisplacementProgressFn &progress,
const std::vector<PrintableColor> &palette, const DisplacementProgressFn &progress,
BakeStageRecorder *debug)
{
TextureDisplacementPrepareResult out;
@@ -4963,13 +4539,10 @@ TextureDisplacementPrepareResult GLGizmoTextureDisplacement::prepare_mesh(
// Colour boundaries need triangles of their own - the chord test cannot see them, since
// the height field is perfectly smooth across a change of filament.
ColorFieldSampler color;
// A flat-colour layer is matched against single filaments only, as the bake does, so its
// boundaries are refined where the bake will actually change filament.
if (params.subdiv_color_edge_mm > 0.f && !color_settings.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(color_settings.palette),
make_palette_quantizer(color_settings.palette_pure));
// The refinement follows perceived colour: a mix is one colour here, however the slicer
// interleaves its filaments layer by layer.
if (params.subdiv_color_edge_mm > 0.f && !palette.empty())
color = make_combined_color_sampler(mesh.its, layers, current, make_palette_quantizer(palette));
// Note the sampler is built on the *quantizer* alone - the refinement follows perceived
// colour, never the interleaving that realises a mix - the slicer does that per layer.
// "Min edge" is a feature-mode control (it is the floor the curvature test refines down
// to); in plain adaptive mode the target edge length is the only criterion, so the floor
// must not be allowed to silently override a target the user set below it.
@@ -5257,9 +4830,9 @@ void GLGizmoTextureDisplacement::rebuild_subdivide_preview()
// Same colour criterion Apply will use, so the previewed wireframe is the mesh that commits.
ColorFieldSampler color;
if (m_subdivide_color_mm > 0.f && any_layer_colors(*mv)) {
const auto [quantize, quantize_pure] = palette_quantizers();
color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets, quantize,
quantize_pure);
cached_palette(); // refreshes m_palette_quantizer if the filaments changed
color = make_combined_color_sampler(mv->mesh().its, mv->texture_displacement_layers, facets,
m_palette_quantizer);
}
its = subdivide_mesh_adaptive(mv->mesh().its, region, m_subdivide_target_mm,
int(mv->mesh().its.indices.size()) + m_subdivide_budget_k * 1000,
@@ -5771,12 +5344,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
return;
ModelVolume *mv = texture_volume();
// The palette also follows project state nobody tells this gizmo about - the filaments and mixed
// slots in the sidebar - so it is checked once a frame, and a change re-runs the previews.
cached_palette();
if (m_palette_changed)
m_preview_params_dirty = true;
float scale = m_parent.get_scale();
#ifdef WIN32
int dpi = get_dpi_for_window(wxGetApp().GetTopWindow());
@@ -6560,15 +6127,6 @@ void GLGizmoTextureDisplacement::on_render_input_window(float x, float y, float
"of filaments can cover a photo or a gradient. An image of flat colors "
"prints the same either way. Off uses one filament per area."));
if (opts.color_mix_enabled) {
cached_palette(); // brings m_palette_filaments up to date
// Every mix can become a filament slot, and there are only so many beside the
// physical filaments.
const int max_mixes = std::max(1, int(EnforcerBlockerType::ExtruderMax) - int(m_palette_filaments.size()));
if (int_row("##color_mix_count", _L("Mixed colors"), &opts.color_mix_count, 1, max_mixes, "%d", card_pad))
m_preview_params_dirty = true;
hover_tip(_u8L("The most mixed filaments a bake may add. They are picked from the "
"texture's colors, and only the ones the bake actually uses are created."));
// After the slider, so a change shows in the same frame.
ImGui::TextDisabled("%s", Slic3r::format(_u8L("%1% printable colors from %2% filaments"),
int(cached_palette().size()), int(m_palette_filaments.size())).c_str());
}
@@ -16,15 +16,11 @@
#include "libslic3r/Color.hpp"
#include <cstddef>
#include "libslic3r/TriangleSelector.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "admesh/stl.h"
#include <cstdint>
#include <functional>
#include "libslic3r/Point.hpp"
#include <imgui.h>
#include <map>
#include <memory>
#include <optional>
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
#include <string>
#include <vector>
@@ -62,7 +58,7 @@ public:
const TextureDisplacementFacetsData &masks,
const std::vector<TextureDisplacementLayer> &layers,
const TextureDisplacementPrepareParams &params,
const TextureColorSettings &color_settings,
const std::vector<PrintableColor> &palette,
const DisplacementProgressFn &progress,
// Optional step capture: receives the mesh
// after the remesh and after the refinement,
@@ -74,43 +70,22 @@ public:
using PaletteEntry = PrintableColor;
// One colour mixes are meant to reach: a bin of the textures' colour histogram, in CIELAB, and how
// much of the image it covers.
struct MixTarget
{
Vec3f lab = Vec3f::Zero();
float weight = 0.f;
};
// The colours worth mixing for: those of the colouring layers in `layers` whose image is not made of
// flat colours (TextureDetail::flat_colors - those print in single filaments only, so no mix could
// serve them). The layers weigh the same and, before only the heaviest bins are kept (which is what
// bounds rank_mixes()), together 1. Read from the image as imported, as analyze_texture_detail()
// does, so the Smoothing slider does not move the palette around.
static std::vector<MixTarget> mix_targets(const std::vector<TextureDisplacementLayer> &layers);
// Mixes of pairs of `filaments`, best first and at most `limit` of them. Each is the one that most
// improves the match to `targets` given those ranked before it, counted only where it beats the
// nearest single filament by the quantizer's prefer-pure margin - which is where the quantizer will
// actually pick it. Stops early once another mix would make no noticeable difference, so a texture
// the filaments already cover gets few mixes or none.
// The printable palette: the loaded filaments (clamped to the sixteen mmu_segmentation_facets can
// address), plus - when `mixing` - every pair of them at evenly spaced ratios.
//
// Ratios are the short-cycle ones (k/d for d up to 6): the slicer interleaves a mix layer by layer,
// and a long cycle prints as visible bands rather than as a colour. A mix's `rgb` is the colour its
// mixed filament slot will show (blend_color_multi(), as the sidebar computes it), so the match, the
// preview and the slot all agree on what the mix looks like.
static std::vector<PaletteEntry> rank_mixes(const std::vector<ColorRGBA> &filaments,
const std::vector<MixTarget> &targets, int limit);
// The first `count` mixes of `ranking` the project can give a filament slot to. A mix `reusable`
// reports as already having a fixed slot costs nothing; any other uses up one of `free_slots`, and
// is skipped once they run out - so the palette never offers a colour a bake could not print.
static std::vector<PaletteEntry> pick_mixes(const std::vector<PaletteEntry> &ranking, int count, int free_slots,
const std::function<bool(const PaletteEntry &)> &reusable);
// The printable palette: the loaded filaments, entry i being filament i, followed by `mixes`.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments,
const std::vector<PaletteEntry> &mixes);
// Mixes are averaged in **CIELAB**, not RGB and not subtractively: two filaments interleaved too
// finely to resolve are averaged by the eye, which is what a perceptual space models. Yellow and
// blue banded together read as a desaturated grey-green, and that is what the preview must promise
// - blending them subtractively would show a green the printer cannot produce this way.
//
// How many ratios depends on how many filaments there are, so the palette stays bounded: the
// quantizer's lookup cube costs one DeltaE00 per cell per entry to fill, and with sixteen
// filaments there are already plenty of colours without mixing any of them.
// `max_entries` bounds the whole palette. It is normally the quantizer's own limit, but when the
// mixes become filament slots it has to be the paint mask's instead: a mask can name only
// EnforcerBlockerType::ExtruderMax states, and every mix now occupies one of them.
static std::vector<PaletteEntry> make_palette(const std::vector<ColorRGBA> &filaments, bool mixing,
int max_entries);
// Maps an image colour to the closest entry of `palette`, perceptually (CIEDE2000 over CIELAB - a
// plain RGB distance picks visibly wrong filaments, most obviously between a saturated colour and
@@ -122,49 +97,40 @@ public:
// to a worker thread and outlives the palette it was built from.
static ColorQuantizeFn make_palette_quantizer(const std::vector<PaletteEntry> &palette);
// The filament (0-based) each entry of `palette` prints in, by palette index: a single filament is
// itself, a mix is the slot `slot_for_mix` returns for it. Only the mixes `triangle_color` actually
// uses (palette index + 1 per triangle, 0 for none) are asked for, since asking is what creates a
// slot. A mix that gets no slot (-1) prints in its dominant component; one nothing uses maps to -1.
//
// This is what the bake writes into the paint: a palette index is a filament only for the single
// filaments, while a mix's slot can sit anywhere among the project's mixed slots.
static std::vector<int> palette_filaments(const std::vector<PaletteEntry> &palette,
const std::vector<uint8_t> &triangle_color,
const std::function<int(const PaletteEntry &)> &slot_for_mix);
// Turns a palette index plus a position into the filament to print there, interleaving the two
// filaments of a mixed entry per `mode`. `layer_height` sizes the Z bands; `cell_mm` the dither
// Everything the jobs need to colour with, for the current volume: the palette, its single-filament
// part, and the despeckle passes. Empty when no layer is actually colouring. Read-only: no filament
// slot is created here, only when a bake commits (see palette_filaments()).
// Everything the jobs need to colour with, for the current volume: palette, mix mode, layer
// height, despeckle. Empty when no layer is actually colouring.
TextureColorSettings color_settings_for(const ModelVolume &mv);
// The printable palette for the current volume and project, rebuilt only when what it depends on
// changes - see the definition for why that caching is not optional.
// The printable palette for the current filaments and mixing setting, rebuilt only when either
// actually changes - see the definition for why that caching is not optional.
const std::vector<PaletteEntry> &cached_palette();
// The quantizers for the cached palette and for its single filaments alone (flat-colour images),
// filled on first use. Only the subdivide preview matches colours on this thread - the jobs build
// their own from the palette they capture - so a palette rebuild, such as every step of a count
// drag, costs no lookup cube unless that preview asks for one.
std::pair<ColorQuantizeFn, ColorQuantizeFn> palette_quantizers();
std::vector<PaletteEntry> m_palette_cache;
ColorQuantizeFn m_palette_quantizer;
ColorQuantizeFn m_palette_pure_quantizer;
// Set when a rebuild changed the palette the previews were drawn with; cleared by rebuild_preview().
bool m_palette_changed = false;
// What the cache was built from.
std::vector<ColorRGBA> m_palette_filaments;
std::vector<std::shared_ptr<std::vector<unsigned char>>> m_palette_images; // the colouring layers' images
std::optional<std::vector<PaletteEntry>> m_mix_ranking; // rank_mixes(), computed on demand
bool m_palette_mixing = false;
int m_palette_mix_count = 0;
int m_palette_free_slots = 0;
std::vector<std::string> m_palette_slots; // the project's mixed slots
// Turns every mix in `palette` into a mixed filament slot and rewrites the entry to name that slot
// as a plain filament, so nothing downstream has to know a mix is involved: is_mix() goes false and
// the resolver simply returns it. The per-layer interleaving then happens in the slicer, where it is
// not limited by how fine the mesh is. Entries whose slot could not be created (the paint-state cap)
// fall back to the nearer of the two components.
void bind_mixes_to_filament_slots(std::vector<PaletteEntry> &palette);
std::vector<PaletteEntry> m_palette_cache;
std::vector<ColorRGBA> m_palette_filaments;
int m_palette_cap = 0; // the max_entries m_palette_cache was built with
bool m_palette_mixing = false;
ColorQuantizeFn m_palette_quantizer;
// The loaded physical filaments, clamped to the states mmu_segmentation_facets can address.
// The loaded filaments, clamped to the sixteen mmu_segmentation_facets can address.
static std::vector<ColorRGBA> filament_palette();
// The Z band height, in mm. One print layer is the ideal, but the interleave is realised per
// *facet*: a band thinner than the mesh can resolve does not dither, it beats against the triangle
// grid and comes out as broad horizontal stripes - and since MMU segmentation reads facet colour,
// it does so in the print too, not only on screen. The refinement edge is chosen from the model's
// diagonal and knows nothing about the layer height, so the band is rounded up to a whole number of
// layers at least two facet rows tall: still exact on the printer, and representable by the mesh
// that has to carry it. Used by both the bake settings and the preview shader, so the two agree.
// The Normal preview's triangles, grouped by the palette entry they will print in. Colour is per facet
// and the palette is small, so the mesh is uploaded once with its index buffer
// The Normal preview's triangles, grouped by the filament they will print in. Colour is per facet
// and there are at most sixteen filaments, so the mesh is uploaded once with its index buffer
// sorted by colour and drawn as one GLModel::render(range) per group - which needs no per-vertex
// colour attribute, and so no change to GLModel's vertex layouts.
//
@@ -177,28 +143,11 @@ public:
ColorRGBA color;
};
std::vector<PreviewColorRun> m_preview_color_runs;
// True if the layer would actually colour something: colour turned on, and a texture that has colour
// to give.
static bool layer_shows_color(const TextureDisplacementLayer &layer);
// True if any of the volume's layers would. What decides whether a palette is captured into a job at
// all, and so whether the colour criterion and the mmu write ever run.
// True if any of the volume's layers would actually colour something: colour turned on, and a
// texture that has colour to give. What decides whether a palette is captured into a job at all,
// and so whether the colour criterion and the mmu write ever run.
static bool any_layer_colors(const ModelVolume &mv);
// The model's own colour paint (mmu_segmentation_facets) as the gizmo draws it over its surface: the
// sub-triangles painted in a filament, grouped by that filament. NONE - the volume's own filament - is
// left out, so those triangles keep the gizmo's neutral, as they do in the preview.
struct PaintedColors
{
indexed_triangle_set facets; // over the paint's whole vertex array
std::vector<int> source; // per triangle of `facets`: the model triangle it lies in
std::vector<int> state; // per triangle of `facets`: its filament state, 1-based
// The triangles of `facets` outside the model triangles `excluded` marks, in order. A model
// triangle past the end of `excluded` is not excluded.
std::vector<size_t> outside(const std::vector<bool> &excluded) const;
};
static PaintedColors painted_colors(const TriangleMesh &mesh, const TriangleSelector::TriangleSplittingData &paint);
void render_painter_gizmo() override;
// Intercepts mouse input while "Adjust Texture" mode is on (dragging the on-canvas offset/
@@ -208,9 +157,6 @@ public:
protected:
void on_render_input_window(float x, float y, float bottom_limit) override;
std::string on_get_name() const override;
// Never in the assemble view: its toolbar does not offer this gizmo, and every preview here is drawn
// with the main canvas's instance transform. The base alone would let the keyboard shortcut open it there.
bool on_is_activable() const override;
wxString handle_snapshot_action_name(bool shift_down, Button button_down) const override;
@@ -730,13 +676,12 @@ private:
GLModel m_shaded_preview_glmodel;
// Translucent tint over the active layer's painted triangles, drawn on top of whichever preview
// is showing - except, between strokes, the active layer's own colour preview, which it would wash
// green (see render_painter_gizmo()). The base painter's own opaque paint highlight
// (render_triangles()) cannot be used in either preview mode - it is coincident with the surface
// and simply covers it - so the only paint feedback the gizmo had was the relief itself, which
// meant erasing showed nothing at all until the stroke ended and the whole preview rebuilt. This
// is that feedback: cheap (the painted patch only), translucent (the preview stays visible through
// it) and rebuilt live during a stroke.
// is showing. The base painter's own opaque paint highlight (render_triangles()) cannot be used
// in either preview mode - it is coincident with the surface and simply covers it - so the only
// paint feedback the gizmo had was the relief itself, which meant erasing showed nothing at all
// until the stroke ended and the whole preview rebuilt. This is that feedback: cheap (the painted
// patch only), translucent (the preview stays visible through it) and rebuilt live during a
// stroke.
GLModel m_paint_overlay_glmodel;
// The islands selected in the UV editor, tinted on the model so the pane's selection can be seen
// in place. Rebuilt whenever the pane's selection differs from the one it was built for.
@@ -754,23 +699,6 @@ private:
GLModel m_other_paint_glmodel;
std::string m_other_paint_key;
void rebuild_other_paint_overlay();
// The model's colour paint, drawn over the surface so the colours a bake wrote stay visible - the
// canvas draws no volume while a paint gizmo is open, and the selectors hold only displacement paint.
// Left out wherever the preview on screen shows paint of its own (`whole_stack`: every layer's, as the
// Normal preview does; otherwise the active layer's), since an opaque overlay there would hide that
// preview. Only while a layer colours: it is the colour workflow's result, and every other paint gizmo
// shows the model neutral.
//
// Two levels: the paint's sub-triangles, which take a selector over the whole mesh and change only with
// the paint itself, and the part drawn, which follows every flushed stroke.
PaintedColors m_painted_colors;
std::string m_painted_colors_key;
GLModel m_painted_colors_glmodel;
std::string m_painted_colors_drawn_key;
std::vector<std::pair<int, std::pair<size_t, size_t>>> m_painted_colors_runs; // filament state, index range
void rebuild_painted_colors(bool whole_stack);
// False when there was nothing to draw.
bool render_painted_colors(bool whole_stack);
// Whether render_shaded_preview_mesh() would actually draw something. Checked before the real volume
// is hidden: with no layer, no texture or no shader the shaded path draws nothing, and hiding the
// volume for it left the model invisible.
@@ -784,9 +712,11 @@ private:
int m_shaded_projection_mode = 0;
Vec3f m_shaded_patch_center = Vec3f::Zero();
Vec3f m_shaded_patch_axis = Vec3f::UnitZ();
// The palette the fast preview matches each fragment against, captured with its mesh. Empty when the
// active layer is not colouring, which is what tells the shader to fall back to the model's own
// colour. Every entry carries the colour it prints in, so drawing it needs nothing else.
// The palette the fast preview's per-triangle filament indices were built against, captured when
// the mesh was. Empty when the active layer is not colouring, which is what tells the shader to
// fall back to the model's own colour. Held rather than re-read at draw time so the indices baked
// into the mesh can never be resolved against a different set of filaments than they were computed
// from - loading a filament mid-session would otherwise recolour a stale preview at random.
std::vector<PaletteEntry> m_shaded_preview_palette;
// GPU island drag: while an island is dragged in the UV editor, the displacement mesh is baked once (with
@@ -4,7 +4,6 @@
#include <functional>
#include <utility>
#include <string>
#include <vector>
#include "libslic3r/TextureDisplacement.hpp"
#include <exception>
#include <cstddef>
@@ -115,17 +114,6 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
if (volume == nullptr)
return;
// The filament each palette entry prints in. This is where a mix becomes a mixed filament slot,
// and only a mix the bake actually painted with: the preview never creates one, so the project
// gains only the slots this result needs. Done before anything below names them - adding a slot
// runs ModelVolume::update_extruder_count(), which clamps paint above the old filament count.
Sidebar &sidebar = plater->sidebar();
const std::vector<int> filament_of = GLGizmoTextureDisplacement::palette_filaments(
m_input.color.palette, m_triangle_color, [&sidebar](const PrintableColor &mix) {
return sidebar.ensure_mixed_filament({ unsigned(mix.a + 1), unsigned(mix.b + 1) },
{ mix.a_percent(), 100 - mix.a_percent() });
});
volume->set_mesh(std::move(m_result));
volume->set_new_unique_id();
volume->calculate_convex_hull();
@@ -141,11 +129,9 @@ void TextureDisplacementBakeJob::finalize(bool canceled, std::exception_ptr &ept
const TriangleSelector::TriangleSplittingData &existing = volume->mmu_segmentation_facets.get_data();
if (!existing.bitstream.empty())
selector.deserialize(existing, false);
for (size_t i = 0; i < m_triangle_color.size(); ++i) {
const size_t entry = size_t(m_triangle_color[i]);
if (entry > 0 && entry <= filament_of.size() && filament_of[entry - 1] >= 0)
selector.set_facet(int(i), EnforcerBlockerType(filament_of[entry - 1] + 1));
}
for (size_t i = 0; i < m_triangle_color.size(); ++i)
if (m_triangle_color[i] > 0)
selector.set_facet(int(i), EnforcerBlockerType(m_triangle_color[i]));
volume->mmu_segmentation_facets.set(selector);
}
@@ -56,9 +56,8 @@ private:
TriangleMesh m_result;
// What the bake spent, for the message it leaves behind when the budget capped the detail.
TextureBakeStats m_stats;
// Per triangle of m_result: the palette entry to print it in, as its index + 1 (0 = leave alone).
// Empty unless a layer asked for colour. See TextureColorRequest; finalize() turns these into
// filaments.
// Per triangle of m_result: the filament to print it in, as an EnforcerBlockerType value
// (0 = leave alone). Empty unless a layer asked for colour. See TextureColorRequest.
std::vector<uint8_t> m_triangle_color;
std::function<void()> m_on_finished;
};
@@ -59,7 +59,7 @@ void TextureDisplacementDebugJob::process(Ctl &ctl)
if (m_input.run_prepare && !m_input.options.pipeline_v2) {
const TextureDisplacementPrepareResult prepared =
GLGizmoTextureDisplacement::prepare_mesh(mesh, masks, m_input.layers, m_input.prepare_params,
m_input.color,
m_input.color.palette,
// Preparation is roughly half the run; the bake
// takes the progress bar from there.
[&report](int pct) { return report(1 + pct / 2); },
@@ -46,7 +46,7 @@ void TextureDisplacementPrepareJob::process(Ctl &ctl)
// idle loop.
int last_reported = 1;
m_result = GLGizmoTextureDisplacement::prepare_mesh(m_input.base_mesh, m_input.masks, m_input.layers,
m_input.params, m_input.color,
m_input.params, m_input.color.palette,
[&ctl, &status, &last_reported](int percent) {
if (ctl.was_canceled())
return false;
@@ -26,13 +26,13 @@ struct TextureDisplacementPreviewInput
// Mesh coordinates -> world millimetres, so the preview is displaced in the same space the bake
// is and the two cannot disagree. See build_texture_displacement().
Transform3d volume_to_world = Transform3d::Identity();
// Empty unless a layer is colouring, in which case the preview reports the palette entry per
// triangle alongside the mesh, so the Normal view shows the colours the bake will produce.
// Empty unless a layer is colouring, in which case the preview reports the filament per triangle
// alongside the mesh, so the Normal view shows what the bake will produce - interleaving included.
TextureColorSettings color;
};
// A preview result: the displaced mesh, and - when the input carried a palette - one palette entry per
// triangle (its index + 1; 0 means "no colour from the texture").
// A preview result: the displaced mesh, and - when the input carried a palette - one filament index
// per triangle (an EnforcerBlockerType value; 0 means "no colour from the texture").
struct TextureDisplacementPreviewResult
{
indexed_triangle_set mesh;
+50 -6
View File
@@ -5048,15 +5048,35 @@ static bool create_mixed_filament_from_result(
is_mixed_opt->values[new_idx] = true;
}
std::string comp_str;
for (size_t i = 0; i < result.components.size(); ++i) {
if (i > 0) comp_str += ",";
comp_str += std::to_string(result.components[i]);
}
{
auto* comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
while (comp_opt->values.size() <= new_idx) comp_opt->values.push_back(std::string{});
comp_opt->values[new_idx] = format_mixed_components(result.components);
comp_opt->values[new_idx] = comp_str;
}
int ratio_sum = 0;
for (int r : result.ratios) ratio_sum += r;
if (ratio_sum <= 0) ratio_sum = 100;
std::string ratio_str;
{
CNumericLocalesSetter c_locale_setter;
for (size_t i = 0; i < result.ratios.size(); ++i) {
if (i > 0) ratio_str += ",";
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", (float)result.ratios[i] / ratio_sum);
ratio_str += buf;
}
}
{
auto* ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
while (ratios_opt->values.size() <= new_idx) ratios_opt->values.push_back(std::string{});
ratios_opt->values[new_idx] = format_mixed_ratios(result.ratios);
ratios_opt->values[new_idx] = ratio_str;
}
if (!project_config.option("filament_mixed_gradient"))
@@ -5116,12 +5136,36 @@ int Sidebar::ensure_mixed_filament(const std::vector<unsigned int> &components,
return -1;
if (p->combos_filament.size() < 2)
return -1;
if (std::accumulate(ratios.begin(), ratios.end(), 0) <= 0)
// Normalise the way create_mixed_filament_from_result() stores them, so the comparison below sees
// the same text the config holds rather than two spellings of one blend.
int ratio_sum = 0;
for (const int r : ratios)
ratio_sum += r;
if (ratio_sum <= 0)
return -1;
if (const int existing = find_fixed_mixed_filament(wxGetApp().preset_bundle->project_config, components, ratios);
existing >= 0)
return existing;
std::string comp_str, ratio_str;
{
CNumericLocalesSetter c_locale_setter;
for (size_t i = 0; i < components.size(); ++i) {
if (i > 0) { comp_str += ","; ratio_str += ","; }
comp_str += std::to_string(components[i]);
char buf[32];
std::snprintf(buf, sizeof(buf), "%.4f", float(ratios[i]) / float(ratio_sum));
ratio_str += buf;
}
}
const auto &project_config = wxGetApp().preset_bundle->project_config;
const auto *is_mixed_opt = project_config.option<ConfigOptionBools>("filament_is_mixed");
const auto *comp_opt = project_config.option<ConfigOptionStrings>("filament_mixed_components");
const auto *ratios_opt = project_config.option<ConfigOptionStrings>("filament_mixed_sublayer_ratios");
if (is_mixed_opt != nullptr && comp_opt != nullptr && ratios_opt != nullptr)
for (size_t i = 0; i < is_mixed_opt->values.size(); ++i)
if (is_mixed_opt->values[i] && i < comp_opt->values.size() && i < ratios_opt->values.size() &&
comp_opt->values[i] == comp_str && ratios_opt->values[i] == ratio_str)
return int(i);
if (wxGetApp().preset_bundle->filament_presets.size() >= size_t(EnforcerBlockerType::ExtruderMax))
return -1;
+3 -4
View File
@@ -292,12 +292,11 @@ public:
// Mixed-color filament sidebar section
void add_mixed_filament();
// The filament slot that blends `components` (1-based physical filament indices) in `ratios`
// (percentages) at a fixed ratio, creating it when no existing fixed mixed slot already describes
// that blend (see find_fixed_mixed_filament()). Returns the 0-based filament index, or -1 when the
// paint-state cap leaves no room for another one.
// (percentages), creating it when no existing mixed slot already describes that blend. Returns the
// 0-based filament index, or -1 when the paint-state cap leaves no room for another one.
//
// Exists so a feature that needs a blend can ask for one without going through the modal dialog:
// a texture displacement bake turns each mix it painted with into a slot, which is what moves the
// the texture displacement gizmo turns each mix in its palette into a slot, which is what moves the
// interleaving from its own paint mask to the slicer, where it happens per layer.
int ensure_mixed_filament(const std::vector<unsigned int> &components, const std::vector<int> &ratios);
void edit_mixed_filament(size_t idx);
+3
View File
@@ -2961,6 +2961,9 @@ void TabPrint::build()
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
optgroup->append_single_option_line("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");
+11
View File
@@ -400,6 +400,17 @@ void PluginPages::relayout()
}
}
void PluginPages::select_page(const PluginCapabilityId& id)
{
if (m_parent == nullptr || m_pages.find(id) == m_pages.end())
return;
// Swap the page into the visible slot first when it lives behind the overflow menu.
m_swapped_in_id = id;
relayout();
m_parent->SelectPageByName(page_tab_id(id));
}
void PluginPages::show_overflow_menu()
{
const int visible_slots = std::max(1, m_visible_page_count);
+2
View File
@@ -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);
+332
View File
@@ -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);
}
+60
View File
@@ -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);
}
+2
View File
@@ -35,7 +35,9 @@ 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_kdtree.cpp
test_multimaterial_segmentation.cpp
test_placeholder_parser.cpp
test_png_read_write.cpp
+88
View File
@@ -7,6 +7,7 @@
#include "libslic3r/libslic3r.h"
#include <numeric>
#include <iostream>
#include <utility>
#include <boost/filesystem.hpp>
#include <utility>
#include <vector>
@@ -300,3 +301,90 @@ TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") {
REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested)));
REQUIRE(top_level_expolygons(reference).size() == 1);
}
// Rings flattened to x,y,x,y... and sorted, with each ring rotated to start at its lowest point: two
// encodings of the same geometry compare equal however the pieces came back or wherever a ring started.
static std::vector<std::vector<coord_t>> canonical_rings(const ExPolygons &expolygons)
{
std::vector<std::vector<coord_t>> rings;
const auto add = [&rings](const Polygon &poly) {
if (poly.points.empty())
return;
Points pts = poly.points;
std::rotate(pts.begin(),
std::min_element(pts.begin(), pts.end(), [](const Point &a, const Point &b) {
return std::make_pair(a.x(), a.y()) < std::make_pair(b.x(), b.y());
}),
pts.end());
std::vector<coord_t> flat;
flat.reserve(pts.size() * 2);
for (const Point &p : pts) {
flat.emplace_back(p.x());
flat.emplace_back(p.y());
}
rings.emplace_back(std::move(flat));
};
for (const ExPolygon &expoly : expolygons) {
add(expoly.contour);
for (const Polygon &hole : expoly.holes)
add(hole);
}
std::sort(rings.begin(), rings.end());
return rings;
}
// The same rings, every coordinate within `tolerance`.
static bool same_rings(const ExPolygons &a, const ExPolygons &b, coord_t tolerance)
{
const std::vector<std::vector<coord_t>> ra = canonical_rings(a), rb = canonical_rings(b);
return std::equal(ra.begin(), ra.end(), rb.begin(), rb.end(), [tolerance](const std::vector<coord_t> &x, const std::vector<coord_t> &y) {
return std::equal(x.begin(), x.end(), y.begin(), y.end(), [tolerance](coord_t u, coord_t v) { return std::abs(u - v) <= tolerance; });
});
}
TEST_CASE("Tiled diff and intersection return the same polygons as the plain calls", "[ClipperUtils]") {
// A grid of disjoint framed squares, enough of them to be split into several tiles.
const int n = 40;
const coord_t cell = scaled<coord_t>(2.), side = scaled<coord_t>(1.5), frame = scaled<coord_t>(0.3);
ExPolygons subject;
for (int y = 0; y < n; ++ y)
for (int x = 0; x < n; ++ x) {
const Point o(x * cell, y * cell);
ExPolygon square(Polygon({ o, o + Point(side, 0), o + Point(side, side), o + Point(0, side) }));
Polygon hole({ o + Point(frame, frame), o + Point(frame, side - frame), o + Point(side - frame, side - frame), o + Point(side - frame, frame) });
square.holes.emplace_back(std::move(hole));
subject.emplace_back(std::move(square));
}
// Clip polygons crossing many squares, one of them large with holes of its own.
Polygons clip;
const coord_t span = n * cell;
for (int i = 0; i < 8; ++ i) {
const coord_t y0 = coord_t(i) * span / 8, y1 = y0 + scaled<coord_t>(0.9);
clip.emplace_back(Polygon({ Point(- cell, y0), Point(span, y0 + cell * 3), Point(span, y1 + cell * 3), Point(- cell, y1) }));
}
ExPolygon big(Polygon({ Point(span / 4, span / 4), Point(3 * span / 4, span / 4), Point(3 * span / 4, 3 * span / 4), Point(span / 4, 3 * span / 4) }));
for (int i = 0; i < 4; ++ i) {
const Point o(span / 4 + scaled<coord_t>(3.1) + i * scaled<coord_t>(9.7), span / 4 + scaled<coord_t>(5.3));
big.holes.emplace_back(Polygon({ o, o + Point(0, scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), 0) }));
}
polygons_append(clip, to_polygons(big));
const ApplySafetyOffset safety = GENERATE(ApplySafetyOffset::No, ApplySafetyOffset::Yes);
// The point of the fixture: below 128 pieces the helpers fall back to a single tile and the tiled
// path under test is never taken.
REQUIRE(ClipperUtils::tile_expolygons(subject, 32).size() > 1);
// With the safety offset a tile unites fewer clip polygons, so Clipper2 can round a crossing 1 unit differently.
const coord_t tolerance = safety == ApplySafetyOffset::Yes ? 1 : 0;
const ExPolygons diff_plain = diff_ex(subject, clip, safety);
const ExPolygons diff_tiled = diff_ex_by_piece(subject, clip, safety);
REQUIRE(area(diff_plain) > 0.);
CHECK(same_rings(diff_tiled, diff_plain, tolerance));
const ExPolygons intersection_plain = intersection_ex(subject, clip, safety);
const ExPolygons intersection_tiled = intersection_ex_by_piece(subject, clip, safety);
REQUIRE(area(intersection_plain) > 0.);
CHECK(same_rings(intersection_tiled, intersection_plain, tolerance));
}
-36
View File
@@ -255,39 +255,3 @@ TEST_CASE("blend_color_multi weights components", "[FilamentMixer]")
REQUIRE(std::abs(comp(mixed, i) - comp("#123456", i)) <= 8);
}
}
TEST_CASE("format_mixed_ratios normalises weights to four decimals", "[FilamentMixer]")
{
REQUIRE(format_mixed_components({1, 3}) == "1,3");
REQUIRE(format_mixed_ratios({50, 50}) == "0.5000,0.5000");
REQUIRE(format_mixed_ratios({1, 2}) == "0.3333,0.6667");
REQUIRE(format_mixed_ratios({1, 1}) == format_mixed_ratios({50, 50}));
}
TEST_CASE("find_fixed_mixed_filament reuses only a fixed slot of the same blend", "[FilamentMixer]")
{
// Physical slots 0 and 1; slot 2 blends them 50:50 as a gradient, slot 3 at a fixed 50:50.
DynamicPrintConfig cfg;
cfg.set_key_value("filament_is_mixed", new ConfigOptionBools({false, false, true, true}));
cfg.set_key_value("filament_mixed_components", new ConfigOptionStrings({"", "", "1,2", "1,2"}));
cfg.set_key_value("filament_mixed_sublayer_ratios",
new ConfigOptionStrings({"", "", format_mixed_ratios({50, 50}), format_mixed_ratios({50, 50})}));
cfg.set_key_value("filament_mixed_gradient", new ConfigOptionBools({false, false, true, false}));
SECTION("The fixed slot is found, whatever scale the weights are given at") {
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == 3);
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {1, 1}) == 3);
}
SECTION("A gradient slot with the same components and ratios is not a match") {
cfg.option<ConfigOptionBools>("filament_is_mixed")->values[3] = false;
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == -1);
}
SECTION("A project without the gradient key still matches its fixed slots") {
cfg.erase("filament_mixed_gradient");
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {50, 50}) == 2);
}
SECTION("Another ratio or another component order is a different blend") {
REQUIRE(find_fixed_mixed_filament(cfg, {1, 2}, {1, 2}) == -1);
REQUIRE(find_fixed_mixed_filament(cfg, {2, 1}, {50, 50}) == -1);
}
}
+292
View File
@@ -0,0 +1,292 @@
#include <catch2/catch_all.hpp>
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <vector>
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
namespace {
ExPolygon rectangle(double x0, double y0, double x1, double y1)
{
return ExPolygon(Points{Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)});
}
// The expolygons stacked in 0.2 mm layers from z = 0 to height.
TpmsRadialField radial_field(const ExPolygons &expolygons, double height, TpmsAdaptiveMode mode = TpmsAdaptiveMode::Lobes)
{
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons});
return TpmsRadialField(slices, get_extents(expolygons), mode, [] {});
}
double radial(const TpmsRadialField &field, const Vec3d &pt)
{
TpmsRadialField::Radials radials;
field.radial(pt, radials);
return radials[0].t;
}
Vec3d center(const TpmsRadialField &field, const Vec3d &pt)
{
TpmsRadialField::Radials radials;
field.radial(pt, radials);
return radials[0].center;
}
// The grid cells are 0.5 mm, so a radial coordinate over 10 mm is accurate to about a twentieth.
constexpr double Tolerance = 0.075;
} // namespace
TEST_CASE("TPMS radial field is zero at the center of a cube and one at its faces", "[FillTpmsAdaptive]")
{
// A 20 mm cube, 10 mm from its center to every face.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
const Vec3d c = center(field, {10., 10., 10.});
CHECK_THAT(c.x(), WithinAbs(10., 0.5));
CHECK_THAT(c.y(), WithinAbs(10., 0.5));
CHECK_THAT(c.z(), WithinAbs(10., 0.5));
CHECK_THAT(radial(field, {10., 10., 10.}), WithinAbs(0., Tolerance));
for (const Vec3d &face : {Vec3d(0., 10., 10.), Vec3d(20., 10., 10.), Vec3d(10., 0., 10.), Vec3d(10., 10., 0.), Vec3d(10., 10., 20.)}) {
CAPTURE(face.x(), face.y(), face.z());
CHECK_THAT(radial(field, face), WithinAbs(1., Tolerance));
}
}
TEST_CASE("TPMS radial field grows linearly from the center of a cube to its faces", "[FillTpmsAdaptive]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
for (double d = 1.; d < 10.; d += 1.) {
CAPTURE(d);
CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance));
CHECK_THAT(radial(field, {10., 10., 10. + d}), WithinAbs(d / 10., Tolerance));
}
}
TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60.);
CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5));
CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(field, {10., 10., 45.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance));
}
TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTpmsAdaptive]")
{
const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)};
const TpmsRadialField field = radial_field(squares, 20.);
CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5));
CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5));
CHECK_THAT(radial(field, {40., 10., 10.}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance));
}
TEST_CASE("TPMS radial field is beyond one outside of the object", "[FillTpmsAdaptive]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 20.);
CHECK(radial(field, {-5., 10., 10.}) > 1.);
CHECK(radial(field, {10., 10., 30.}) > 1.);
}
TEST_CASE("TPMS radial field grades every lobe of a body towards its own center", "[FillTpmsAdaptive]")
{
// Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep.
const Vec3d c1(10., 10., 10.), c2(26., 10., 10.);
std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
for (const Vec3d &c : {c1, c2}) {
Polygon &circle = circles.emplace_back();
for (int k = 0; k < 90; ++k)
circle.points.push_back(Point::new_scale(c.x() + r * std::cos(k * 2. * PI / 90.), c.y() + r * std::sin(k * 2. * PI / 90.)));
}
layers.push_back(union_ex(circles));
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
for (const Vec3d &c : {c1, c2}) {
CAPTURE(c.x());
CHECK_THAT(center(field, c).x(), WithinAbs(c.x(), 0.5));
CHECK_THAT(radial(field, c), WithinAbs(0., Tolerance));
CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance));
}
// The side between the lobes is half way to the surface, where both patterns morph into each other.
TpmsRadialField::Radials radials;
REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2);
for (size_t i = 0; i < 2; ++i) {
CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance));
CHECK_THAT(radials[i].weight, WithinAbs(0.5, 0.05));
}
}
TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously", "[FillTpmsAdaptive]")
{
// Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle.
const std::array<Vec3d, 3> centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)};
std::vector<ExPolygons> layers;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
for (const Vec3d &c : centers) {
Polygon &circle = circles.emplace_back();
for (int k = 0; k < 90; ++k)
circle.points.push_back(Point::new_scale(c.x() + r * std::cos(k * 2. * PI / 90.), c.y() + r * std::sin(k * 2. * PI / 90.)));
}
layers.push_back(union_ex(circles));
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
// Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly.
const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.;
size_t max_count = 0;
double max_jump = 0.;
double max_error = 0.;
for (int row = 0; row <= 100; ++row) {
std::array<float, 3> previous{};
for (int step = 0; step <= 200; ++step) {
TpmsRadialField::Radials radials;
const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials);
max_count = std::max(max_count, count);
std::array<float, 3> weights{};
for (size_t i = 0; i < count; ++i) {
auto nearest = std::min_element(centers.begin(), centers.end(), [&](const Vec3d &a, const Vec3d &b) {
return (a - radials[i].center).norm() < (b - radials[i].center).norm();
});
weights[nearest - centers.begin()] += radials[i].weight;
}
max_error = std::max(max_error, std::abs(weights[0] + weights[1] + weights[2] - 1.));
if (step > 0)
for (size_t k = 0; k < 3; ++k)
max_jump = std::max(max_jump, double(std::abs(weights[k] - previous[k])));
previous = weights;
}
}
CHECK(max_count == 3);
CHECK(max_error < 1e-5);
CHECK(max_jump < 0.05);
}
TEST_CASE("TPMS radial field is empty when the object is thinner than the grid cells", "[FillTpmsAdaptive]")
{
// A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more.
const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)};
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 1000; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &bar});
const TpmsRadialField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
TpmsAdaptiveMode::Lobes, [] {});
CHECK(field.empty());
}
TEST_CASE("TPMS depth follows the distance to the surface relative to the deepest point", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField field = radial_field(square, 60., TpmsAdaptiveMode::SmoothBlend);
for (double z : {15., 30., 45.}) {
CAPTURE(z);
CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance));
}
CHECK_THAT(field.depth({5., 10., 30.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(field.depth({10., 10., 55.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance));
}
TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the surface", "[FillTpmsAdaptive]")
{
// In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField cube = radial_field(square, 20., TpmsAdaptiveMode::DistanceWarp);
CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance));
// 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep.
const TpmsRadialField lobes = radial_field(square, 60.);
const TpmsRadialField warp = radial_field(square, 60., TpmsAdaptiveMode::DistanceWarp);
for (double z : {15., 45.}) {
CAPTURE(z);
CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance));
CHECK(radial(warp, {10., 10., z}) < 0.25);
}
CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance));
}
TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards", "[FillTpmsAdaptive]")
{
// The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside.
const ExPolygons square{rectangle(0., 0., 40., 40.)};
const TpmsRadialField field = radial_field(square, 40., TpmsAdaptiveMode::SteppedShells);
const std::vector<TpmsShell> shells = make_tpms_shells(field, square.front(), 20., 0.2f, 0.05f, TpmsAdaptiveGradient::Linear);
REQUIRE(shells.size() == 5);
CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6));
CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6));
double area = 0.;
for (size_t i = 0; i < shells.size(); ++i) {
CAPTURE(i);
if (i > 0)
CHECK(shells[i].density < shells[i - 1].density);
for (const ExPolygon &expolygon : shells[i].expolygons)
area += expolygon.area();
}
CHECK_THAT(area / square.front().area(), WithinAbs(1., 0.01));
const Point center = Point::new_scale(20., 20.);
CHECK(std::any_of(shells.back().expolygons.begin(), shells.back().expolygons.end(),
[&center](const ExPolygon &expolygon) { return expolygon.contains(center); }));
}
TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on its own", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const TpmsRadialField normal_z = radial_field(square, 60., TpmsAdaptiveMode::NormalZ);
for (double z : {5., 30., 55.}) {
CAPTURE(z);
CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(normal_z, {15., 10., z}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(normal_z, {10., 0., z}), WithinAbs(1., Tolerance));
}
// Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const TpmsRadialField normal_x = radial_field(square, 60., TpmsAdaptiveMode::NormalX);
for (double x : {3., 10., 17.}) {
CAPTURE(x);
CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(normal_x, {x, 10., 45.}), WithinAbs(0.5, Tolerance));
}
}
+67
View File
@@ -0,0 +1,67 @@
#include <catch2/catch_all.hpp>
#include <numeric>
#include <random>
#include <vector>
#include "libslic3r/KDTreeIndirect.hpp"
#include "libslic3r/Point.hpp"
using namespace Slic3r;
TEST_CASE("Visiting the nearby points gives what collecting them gives", "[KDTree]") {
std::mt19937 rng(19937);
std::uniform_real_distribution<float> coord(-50.f, 50.f);
// Points in a box, so that a radius search returns anything from none of them to all of them.
std::vector<Vec3f> points(2000);
for (Vec3f &p : points)
p = Vec3f(coord(rng), coord(rng), coord(rng));
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
std::vector<size_t> indices(points.size());
std::iota(indices.begin(), indices.end(), 0);
tree.build(indices);
const float radius = GENERATE(0.5f, 5.f, 25.f, 200.f);
for (int i = 0; i < 20; ++ i) {
const Vec3f center(coord(rng), coord(rng), coord(rng));
const std::vector<size_t> collected = find_nearby_points(tree, center, radius);
std::vector<size_t> visited;
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
// Same points, and in the same order: a caller that keeps the first of several equally good ones
// must get the same answer either way.
REQUIRE(visited == collected);
}
}
TEST_CASE("A radius search returns every point within the radius and no other", "[KDTree]") {
std::mt19937 rng(2024);
std::uniform_real_distribution<float> coord(-20.f, 20.f);
std::vector<Vec3f> points(500);
for (Vec3f &p : points)
p = Vec3f(coord(rng), coord(rng), coord(rng));
auto coordinate = [&points](size_t idx, size_t dimension) { return points[idx](int(dimension)); };
KDTreeIndirect<3, float, decltype(coordinate)> tree(coordinate);
std::vector<size_t> indices(points.size());
std::iota(indices.begin(), indices.end(), 0);
tree.build(indices);
const Vec3f center(1.f, -2.f, 3.f);
const float radius = 7.f;
std::vector<size_t> expected;
for (size_t i = 0; i < points.size(); ++ i)
if ((points[i] - center).squaredNorm() < radius * radius)
expected.emplace_back(i);
std::vector<size_t> visited;
visit_nearby_points(tree, center, radius, [&visited](size_t idx) { visited.emplace_back(idx); });
std::sort(visited.begin(), visited.end());
REQUIRE(! expected.empty());
REQUIRE(visited == expected);
}
@@ -68,28 +68,6 @@ static std::shared_ptr<std::vector<unsigned char>> make_flat_gray_png(uint8_t va
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// The same round trip for a flat colour image, which decode_height_texture() reads through its colour path.
static std::shared_ptr<std::vector<unsigned char>> make_flat_rgb_png(uint8_t r, uint8_t g, uint8_t b, size_t w = 4, size_t h = 4)
{
std::vector<uint8_t> rgb;
for (size_t i = 0; i < w * h; ++i)
rgb.insert(rgb.end(), { r, g, b });
const boost::filesystem::path tmp_path = boost::filesystem::temp_directory_path()
/ boost::filesystem::unique_path("texdisp_test_%%%%%%%%.png");
REQUIRE(Slic3r::png::write_rgb_to_file(tmp_path.string(), w, h, rgb));
std::vector<unsigned char> bytes;
{
std::ifstream ifs(tmp_path.string(), std::ios::binary);
bytes.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
}
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
REQUIRE_FALSE(bytes.empty());
return std::make_shared<std::vector<unsigned char>>(std::move(bytes));
}
// A hard-edged black/white checkerboard, the worst case for a height map: every texel boundary is a
// step, which is precisely the relief the post-process smoothing exists to round off.
static std::shared_ptr<std::vector<unsigned char>> make_checkerboard_png(size_t w = 16, size_t h = 16)
@@ -2157,20 +2135,3 @@ TEST_CASE("A second bake beside a first comes out as fine as a single bake", "[T
CHECK(second <= single * 5 / 4);
}
TEST_CASE("whether a layer's texture has colour agrees with its decode", "[TextureDisplacement]")
{
TextureDisplacementLayer gray;
gray.image_data = make_flat_gray_png(128);
CHECK_FALSE(height_texture_has_color(gray));
TextureDisplacementLayer color;
color.image_data = make_flat_rgb_png(200, 40, 10);
// Before the image is decoded and after, and whatever the smoothing.
CHECK(height_texture_has_color(color));
CHECK(decode_height_texture(color).has_color());
CHECK(height_texture_has_color(color));
color.smoothing = 0.5f;
CHECK(height_texture_has_color(color));
CHECK_FALSE(height_texture_has_color(TextureDisplacementLayer{}));
}
-1
View File
@@ -36,7 +36,6 @@ add_executable(${_TEST_NAME}_tests
test_plugin_audit.cpp
test_plugin_json_depth.cpp
test_shortcuts.cpp
test_texture_color_palette.cpp
test_file_url.cpp
test_user_manager.cpp
../fff_print/test_helpers.cpp
@@ -1,294 +0,0 @@
// The texture displacement gizmo's palette helpers live in libslic3r_gui; this is the suite that links it.
// Same Windows include prologue as test_filament_bitmap_utils.cpp (wx pulls in <windows.h>; keep
// WIN32_LEAN_AND_MEAN / NOMINMAX ahead of the Catch2 headers).
#ifdef WIN32
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <Windows.h>
#endif
#include <cstddef>
#include <cstdint>
#include <fstream>
#include <ios>
#include <iterator>
#include <memory>
#include <string>
#include <utility>
#include <vector>
#include <catch2/catch_all.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_floating_point.hpp>
#include "libslic3r/Color.hpp"
#include "libslic3r/FilamentMixer.hpp"
#include "libslic3r/PNGReadWrite.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/TextureDisplacement.hpp"
#include "libslic3r/TriangleMesh.hpp"
#include "libslic3r/TriangleSelector.hpp"
#include "slic3r/GUI/Gizmos/GLGizmoTextureDisplacement.hpp"
#include "slic3r/Utils/ColorSpaceConvert.hpp"
#include "test_utils.hpp"
using namespace Slic3r;
using Catch::Matchers::WithinAbs;
using Gizmo = Slic3r::GUI::GLGizmoTextureDisplacement;
using Entry = Gizmo::PaletteEntry;
using MixTarget = Gizmo::MixTarget;
namespace {
const ColorRGBA BLACK{ 0.f, 0.f, 0.f, 1.f };
const ColorRGBA WHITE{ 1.f, 1.f, 1.f, 1.f };
const ColorRGBA RED{ 1.f, 0.f, 0.f, 1.f };
const ColorRGBA BLUE{ 0.f, 0.f, 1.f, 1.f };
const ColorRGBA YELLOW{ 1.f, 1.f, 0.f, 1.f };
MixTarget target(const Vec3f &rgb, float weight)
{
MixTarget t;
RGB2Lab(rgb.x(), rgb.y(), rgb.z(), &t.lab.x(), &t.lab.y(), &t.lab.z());
t.weight = weight;
return t;
}
// The colour a mixed slot of these two filaments shows, as the sidebar computes it.
Vec3f slot_color(const ColorRGBA &a, const ColorRGBA &b, int a_percent)
{
ColorRGB c;
REQUIRE(decode_color(blend_color_multi({ encode_color(a), encode_color(b) }, { a_percent, 100 - a_percent }), c));
return Vec3f(c.r(), c.g(), c.b());
}
// A colour image layer, through Slic3r's own PNG writer so decode_height_texture() reads it the way it
// reads an imported texture.
TextureDisplacementLayer color_layer(int w, int h, const std::vector<uint8_t> &rgb)
{
ScopedTemporaryFile png(".png");
REQUIRE(png::write_rgb_to_file(png.string(), size_t(w), size_t(h), rgb));
std::ifstream in(png.string(), std::ios::binary);
std::vector<unsigned char> bytes{ std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>() };
REQUIRE_FALSE(bytes.empty());
TextureDisplacementLayer layer;
layer.image_data = std::make_shared<std::vector<unsigned char>>(std::move(bytes));
layer.color_enabled = true;
return layer;
}
// A red/green ramp over a fixed blue: colours spread over many bins, so the image is not flat-colour.
TextureDisplacementLayer gradient_layer()
{
const int n = 64;
std::vector<uint8_t> rgb;
for (int y = 0; y < n; ++y)
for (int x = 0; x < n; ++x) {
rgb.push_back(uint8_t(x * 4));
rgb.push_back(uint8_t(y * 4));
rgb.push_back(128);
}
return color_layer(n, n, rgb);
}
// A 2048x1100 image, over mix_targets()' sampling budget, that is pure red wherever `red(x, y)` holds and
// elsewhere a gradient spread over far more than eight coarse bins, so the image is not flat-colour.
template<class RedFn> TextureDisplacementLayer striped_layer(RedFn red)
{
const int w = 2048, h = 1100;
std::vector<uint8_t> rgb;
rgb.reserve(size_t(w) * size_t(h) * 3);
for (int y = 0; y < h; ++y)
for (int x = 0; x < w; ++x) {
const bool is_red = red(x, y);
rgb.push_back(is_red ? 255 : uint8_t(x * 255 / w));
rgb.push_back(is_red ? 0 : uint8_t(y * 255 / h));
rgb.push_back(is_red ? 0 : 128);
}
return color_layer(w, h, rgb);
}
// How much of the targets' weight is pure red.
float red_weight(const std::vector<MixTarget> &targets)
{
Vec3f red;
RGB2Lab(1.f, 0.f, 0.f, &red.x(), &red.y(), &red.z());
float weight = 0.f;
for (const MixTarget &t : targets)
if ((t.lab - red).norm() < 3.f)
weight += t.weight;
return weight;
}
} // namespace
TEST_CASE("mix targets cover each colouring photo layer once and skip the rest", "[TextureColorPalette][TextureDisplacement]")
{
TextureDisplacementLayer photo = gradient_layer();
const std::vector<MixTarget> targets = Gizmo::mix_targets({ photo });
REQUIRE_FALSE(targets.empty());
float total = 0.f;
for (const MixTarget &t : targets)
total += t.weight;
REQUIRE_THAT(total, WithinAbs(1., 1e-4));
SECTION("A layer that does not colour gives no targets") {
photo.color_enabled = false;
REQUIRE(Gizmo::mix_targets({ photo }).empty());
}
SECTION("A flat-colour image prints in single filaments, so it gives no targets") {
const TextureDisplacementLayer flat = color_layer(8, 8, std::vector<uint8_t>(8 * 8 * 3, 200));
REQUIRE(Gizmo::mix_targets({ flat }).empty());
}
}
TEST_CASE("mix targets of a large image weigh each colour by its share, whatever its stripes", "[TextureColorPalette][TextureDisplacement]")
{
// Sampled rather than read in full, these must not line up with the samples: a fixed sampling step
// sees only one phase of a stripe pattern, and a generator whose offsets repeat sees only some.
SECTION("Red on every other column") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int) { return x % 2 == 0; }) })),
WithinAbs(1. / 2., 0.03));
}
SECTION("Red on every fourth column") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int) { return x % 4 == 0; }) })),
WithinAbs(1. / 4., 0.03));
}
SECTION("Red on diagonals") {
CHECK_THAT(red_weight(Gizmo::mix_targets({ striped_layer([](int x, int y) { return (x - y) % 3 == 0; }) })),
WithinAbs(1. / 3., 0.03));
}
}
TEST_CASE("the mix ranked first is the one the image needs most", "[TextureColorPalette][TextureDisplacement]")
{
// The whole image is exactly the colour of a 1:1 black/white slot.
const std::vector<MixTarget> targets = { target(slot_color(BLACK, WHITE, 50), 1.f) };
const std::vector<Entry> ranked = Gizmo::rank_mixes({ BLACK, WHITE }, targets, 1);
REQUIRE(ranked.size() == 1);
CHECK(ranked.front().a == 0);
CHECK(ranked.front().b == 1);
CHECK(ranked.front().num * 2 == ranked.front().den);
}
TEST_CASE("an image the filaments already match ranks no mixes", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<MixTarget> targets = { target(Vec3f(1.f, 0.f, 0.f), 0.5f), target(Vec3f(0.f, 0.f, 1.f), 0.5f) };
REQUIRE(Gizmo::rank_mixes({ RED, BLUE }, targets, 8).empty());
}
TEST_CASE("ranked mixes stay within the limit and show their slot's colour", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<ColorRGBA> filaments = { RED, BLUE, YELLOW };
const std::vector<MixTarget> targets = Gizmo::mix_targets({ gradient_layer() });
for (const int limit : { 1, 3 }) {
const std::vector<Entry> ranked = Gizmo::rank_mixes(filaments, targets, limit);
CHECK(int(ranked.size()) <= limit);
CHECK_FALSE(ranked.empty());
}
const std::vector<Entry> ranked = Gizmo::rank_mixes(filaments, targets, 6);
for (const Entry &e : ranked) {
REQUIRE(e.is_mix());
const Vec3f expected = slot_color(filaments[size_t(e.a)], filaments[size_t(e.b)], e.a_percent());
CHECK_THAT(e.rgb.x(), WithinAbs(expected.x(), 1e-6));
CHECK_THAT(e.rgb.y(), WithinAbs(expected.y(), 1e-6));
CHECK_THAT(e.rgb.z(), WithinAbs(expected.z(), 1e-6));
}
}
TEST_CASE("picked mixes never need more slots than the project has free", "[TextureColorPalette][TextureDisplacement]")
{
const std::vector<Entry> ranking = { { Vec3f::Zero(), 0, 1, 1, 2 }, { Vec3f::Zero(), 0, 1, 1, 3 }, { Vec3f::Zero(), 0, 1, 2, 3 } };
const auto reusable_second = [](const Entry &e) { return e.num == 1 && e.den == 3; };
SECTION("The count caps the pick") {
REQUIRE(Gizmo::pick_mixes(ranking, 2, 10, nullptr).size() == 2);
}
SECTION("With no free slot only a mix that already has one is kept") {
const std::vector<Entry> picked = Gizmo::pick_mixes(ranking, 3, 0, reusable_second);
REQUIRE(picked.size() == 1);
CHECK(picked.front().den == 3);
CHECK(picked.front().num == 1);
}
SECTION("A reusable mix costs no free slot") {
const std::vector<Entry> picked = Gizmo::pick_mixes(ranking, 3, 1, reusable_second);
REQUIRE(picked.size() == 2);
CHECK(picked[0].den == 2);
CHECK(picked[1].den == 3);
}
}
TEST_CASE("a baked mix paints the slot it was given, wherever that slot sits", "[TextureColorPalette][TextureDisplacement]")
{
// Two filaments, then two mixes of them. Palette index 2 is a mix, but its slot need not be
// filament 2: a project that already holds other mixed slots puts it further along.
const std::vector<Entry> palette = Gizmo::make_palette({ BLACK, WHITE }, { { Vec3f::Zero(), 0, 1, 1, 2 }, { Vec3f::Zero(), 0, 1, 1, 3 } });
REQUIRE(palette.size() == 4);
// Filament 0, the 1:1 mix twice, nothing: the 1:2 mix is never used.
const std::vector<uint8_t> triangle_color = { 1, 3, 3, 0 };
std::vector<Entry> asked;
const auto slot_seven = [&asked](const Entry &e) {
asked.push_back(e);
return 7;
};
const std::vector<int> filament = Gizmo::palette_filaments(palette, triangle_color, slot_seven);
REQUIRE(filament.size() == 4);
CHECK(filament[0] == 0);
CHECK(filament[1] == 1);
CHECK(filament[2] == 7);
CHECK(filament[3] == -1);
// Asking creates a slot, so an unused mix is never asked for.
REQUIRE(asked.size() == 1);
CHECK(asked.front().den == 2);
SECTION("A mix that gets no slot prints in its dominant component") {
const std::vector<int> fallback = Gizmo::palette_filaments(palette, { 3, 4 }, [](const Entry &) { return -1; });
CHECK(fallback[2] == 0); // 1:1 - the first component
CHECK(fallback[3] == 1); // 1 part black in 3 - white dominates
}
}
TEST_CASE("the model's colour paint is drawn by filament, and only where no layer paint covers it", "[TextureColorPalette][TextureDisplacement]")
{
// A strip of four triangles: filament 2 on the first and last, filament 5 on the second, and the third
// left to the volume's own filament.
indexed_triangle_set strip;
strip.vertices = { Vec3f(0, 0, 0), Vec3f(1, 0, 0), Vec3f(0, 1, 0), Vec3f(1, 1, 0), Vec3f(0, 2, 0), Vec3f(1, 2, 0) };
strip.indices = { stl_triangle_vertex_indices(0, 1, 2), stl_triangle_vertex_indices(1, 3, 2), stl_triangle_vertex_indices(2, 3, 4),
stl_triangle_vertex_indices(3, 5, 4) };
const TriangleMesh mesh(strip);
TriangleSelector paint(mesh);
paint.set_facet(0, EnforcerBlockerType(2));
paint.set_facet(1, EnforcerBlockerType(5));
paint.set_facet(3, EnforcerBlockerType(2));
const Gizmo::PaintedColors colors = Gizmo::painted_colors(mesh, paint.serialize());
// Grouped by filament; the triangle in the volume's own filament is never drawn.
REQUIRE(colors.facets.indices.size() == 3);
REQUIRE(colors.source.size() == 3);
REQUIRE(colors.state.size() == 3);
CHECK(colors.state == std::vector<int>{ 2, 2, 5 });
CHECK(colors.source == std::vector<int>{ 0, 3, 1 });
SECTION("A model triangle a layer's paint covers is left to the preview") {
std::vector<bool> excluded(mesh.its.indices.size(), false);
excluded[3] = true;
const std::vector<size_t> kept = colors.outside(excluded);
REQUIRE(kept.size() == 2);
CHECK(colors.source[kept[0]] == 0);
CHECK(colors.source[kept[1]] == 1);
}
SECTION("A mask shorter than the model leaves the rest drawn") {
CHECK(colors.outside({ true }).size() == 2);
}
}