Compare commits

..
Author SHA1 Message Date
SoftFever 55b8fe296f Merge branch 'main' into perf/slicing-optimizations 2026-10-20 19:37:06 +08:00
raistlin7447 17c3bbb7b6 Merge branch 'main' into perf/slicing-optimizations 2026-10-19 11:33:03 -05:00
Rodrigo Faselli c030131bcc Merge branch 'main' into perf/slicing-optimizations 2026-10-11 22:39:31 -03:00
Rodrigo Faselli ac8ac299a5 add missing headers
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
2026-10-11 22:36:16 -03:00
Rodrigo Faselli 0f48982b78 Merge branch 'main' into perf/slicing-optimizations 2026-10-11 15:00:58 -03:00
SoftFever f6e8821b13 Merge branch 'main' into perf/slicing-optimizations 2026-10-10 01:09:46 +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
43 changed files with 1902 additions and 2313 deletions

No files matched your search

-1
View File
@@ -123,7 +123,6 @@ option(SLIC3R_ASAN "Enable ASan on Clang and GCC" 0)
# Python stubgen module # Python stubgen module
option(ORCA_BUILD_PYTHON_STUBGEN_MODULE "Build importable Python module for pybind11-stubgen" ON) option(ORCA_BUILD_PYTHON_STUBGEN_MODULE "Build importable Python module for pybind11-stubgen" ON)
option(ORCA_BUILD_PLUGIN_PERMISSION_PROBE "Build the headless Python plugin permission probe (developer tool)" OFF)
set(ORCA_BUNDLED_UV_EXECUTABLE "" CACHE FILEPATH "Path to a uv executable to bundle for Python package installation. Leave empty to auto-download.") set(ORCA_BUNDLED_UV_EXECUTABLE "" CACHE FILEPATH "Path to a uv executable to bundle for Python package installation. Leave empty to auto-download.")
# Auto-download uv if not provided by the user. # Auto-download uv if not provided by the user.
+17
View File
@@ -62,6 +62,23 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
clip do not leave slivers. clip do not leave slivers.
- Open polylines are clipped with the non-zero rule and keep their direction. - 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 ### Offsets
- Before offsetting, input vertices closer than - Before offsetting, input vertices closer than
-1
View File
@@ -366,7 +366,6 @@ src/slic3r/Utils/MoonrakerPrinterAgent.cpp
src/slic3r/Utils/BBLNetworkPlugin.cpp src/slic3r/Utils/BBLNetworkPlugin.cpp
src/slic3r/Utils/OrcaCloudServiceAgent.cpp src/slic3r/Utils/OrcaCloudServiceAgent.cpp
src/slic3r/plugin/PluginAuditManager.cpp src/slic3r/plugin/PluginAuditManager.cpp
src/slic3r/plugin/PluginAuditManagerGui.cpp
src/slic3r/plugin/PluginConfig.cpp src/slic3r/plugin/PluginConfig.cpp
src/slic3r/plugin/PluginResolver.cpp src/slic3r/plugin/PluginResolver.cpp
src/slic3r/plugin/CloudPluginService.cpp src/slic3r/plugin/CloudPluginService.cpp
+1 -1
View File
@@ -23,7 +23,7 @@ if %FULL_MODE%==1 (
call :prepareGettextList "%list_file%" "%filtered_list%" "%missing_list%" call :prepareGettextList "%list_file%" "%filtered_list%" "%missing_list%"
if "!has_sources!"=="1" ( if "!has_sources!"=="1" (
if not exist "%generated_i18n%" mkdir "%generated_i18n%" if not exist "%generated_i18n%" mkdir "%generated_i18n%"
.\tools\xgettext.exe --keyword=L --keyword=_L --keyword=_u8L --keyword=PLUGIN_U8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "%filtered_list%" -o "%generated_pot%" .\tools\xgettext.exe --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "%filtered_list%" -o "%generated_pot%"
if errorlevel 1 ( if errorlevel 1 (
set "script_exit_code=1" set "script_exit_code=1"
) else ( ) else (
+1 -1
View File
@@ -85,7 +85,7 @@ if $FULL_MODE; then
generated_pot_file="${generated_i18n_dir}/OrcaSlicer.pot" generated_pot_file="${generated_i18n_dir}/OrcaSlicer.pot"
mkdir -p "$generated_i18n_dir" mkdir -p "$generated_i18n_dir"
xgettext --keyword=L --keyword=_L --keyword=_u8L --keyword=PLUGIN_U8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "$filtered_list" -o "$generated_pot_file" xgettext --keyword=L --keyword=_L --keyword=_u8L --keyword=L_CONTEXT:1,2c --keyword=_L_CONTEXT:1,2c --keyword=_u8L_CONTEXT:1,2c --keyword=_L_PLURAL:1,2 --add-comments=TRN --from-code=UTF-8 --no-location --debug --boost --no-wrap -f "$filtered_list" -o "$generated_pot_file"
python3 scripts/HintsToPot.py ./resources "$generated_i18n_dir" python3 scripts/HintsToPot.py ./resources "$generated_i18n_dir"
if [ -f "$pot_file" ] && files_equal_ignoring_pot_date "$pot_file" "$generated_pot_file"; then if [ -f "$pot_file" ] && files_equal_ignoring_pot_date "$pot_file" "$generated_pot_file"; then
+68
View File
@@ -9,6 +9,8 @@
#include <numeric> #include <numeric>
#include <unordered_map> #include <unordered_map>
#include <tbb/parallel_for.h>
#include "ClipperUtils.hpp" #include "ClipperUtils.hpp"
#include "BoundingBox.hpp" #include "BoundingBox.hpp"
#include "ExPolygon.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); } { 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) 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); } { 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). // 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) Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, 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 "Polyline.hpp"
#include "Line.hpp" #include "Line.hpp"
#include "libslic3r.h" #include "libslic3r.h"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp" #include "ExPolygon.hpp"
#include "Polygon.hpp" #include "Polygon.hpp"
#include "Surface.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 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); [[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 // 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::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::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); 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::Polylines &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &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); Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
+31 -12
View File
@@ -22,6 +22,8 @@
#include "../PrintConfig.hpp" #include "../PrintConfig.hpp"
#include "../Surface.hpp" #include "../Surface.hpp"
#include <tbb/parallel_for.h>
#include "AABBTreeLines.hpp" #include "AABBTreeLines.hpp"
#include "libslic3r/Config.hpp" #include "libslic3r/Config.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
@@ -675,24 +677,28 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
if (!line_based_pattern) { if (!line_based_pattern) {
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing); 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); Polygons filled_area = to_polygons(expolygon);
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon. // "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)); Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
if (inner_area.empty()) { if (inner_area.empty()) {
narrow_infill.emplace_back(expolygon); split_parts[idx].second.emplace_back(expolygon);
continue; return;
} }
ExPolygons inner_ex = union_ex(inner_area); ExPolygons inner_ex = union_ex(inner_area);
ExPolygons expolys{expolygon}; ExPolygons expolys{expolygon};
ExPolygons narrow_ex = diff_ex(expolys, inner_ex); split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
ExPolygons normal_ex = intersection_ex(expolys, inner_ex); split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
});
append(normal_infill, normal_ex); // normal infill area for (auto &[normal_ex, narrow_ex] : split_parts) {
append(narrow_infill, narrow_ex); // narrow infill area append(normal_infill, std::move(normal_ex));
append(narrow_infill, std::move(narrow_ex));
} }
return; return;
@@ -714,7 +720,10 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
} }
const double aligning_angle = -base_angle + PI; 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 filled_area = to_polygons(expolygon);
polygons_rotate(filled_area, aligning_angle); polygons_rotate(filled_area, aligning_angle);
BoundingBox bb = get_extents(filled_area); BoundingBox bb = get_extents(filled_area);
@@ -845,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); polygons_rotate(normal_fill_areas, -aligning_angle);
@@ -1473,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->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr; 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) { if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x(); params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis); expoly.symmetric_y(params.symmetric_y_axis);
+78 -5
View File
@@ -6,6 +6,7 @@
#include "../ShortestPath.hpp" #include "../ShortestPath.hpp"
#include "libslic3r/Point.hpp" #include "libslic3r/Point.hpp"
#include "libslic3r/PrintConfig.hpp" #include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm> #include <algorithm>
#include <cassert> #include <cassert>
@@ -141,15 +142,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()}; 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. // Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments. // 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; bool improved = false;
while (max_iters-- > 0) { // Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on
auto [ci, cj] = find_crossing(); // islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same
if (ci == std::numeric_limits<size_t>::max()) break; // orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps
improved = true; // 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); 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; return improved;
} }
+18 -13
View File
@@ -29,6 +29,7 @@
#include <memory> #include <memory>
#include <random> #include <random>
#include <algorithm> #include <algorithm>
#include <limits>
#include <queue> #include <queue>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
@@ -1207,21 +1208,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 size_t layer_idx, const float max_distance,
const SeamPlacerImpl::SeamComparator &comparator) const { const SeamPlacerImpl::SeamComparator &comparator) const {
using namespace SeamPlacerImpl; using namespace SeamPlacerImpl;
std::vector<size_t> nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position, // Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within
max_distance); // 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();
if (nearby_points_indices.empty()) { size_t best_nearby_point_index = none;
return {}; 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 best_nearby_point_index = nearby_points_indices[0]; (size_t nearby_point_index) {
size_t nearest_point_index = nearby_points_indices[0]; if (best_nearby_point_index == none) {
// The first point found starts both, as the first of the collected ones did.
// Now find best nearby point, nearest point, and corresponding indices best_nearby_point_index = nearest_point_index = nearby_point_index;
for (const size_t &nearby_point_index : nearby_points_indices) { }
const SeamCandidate &point = layers[layer_idx].points[nearby_point_index]; const SeamCandidate &point = layers[layer_idx].points[nearby_point_index];
if (point.perimeter.finalized) { 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], if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index],
projected_position.head<2>()) projected_position.head<2>())
@@ -1233,6 +1234,10 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|| layers[layer_idx].points[nearest_point_index].perimeter.finalized) { || layers[layer_idx].points[nearest_point_index].perimeter.finalized) {
nearest_point_index = nearby_point_index; 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]; 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; 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> template<typename KDTreeIndirectType, typename PointType>
std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center, std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType& max_distance) const typename KDTreeIndirectType::CoordType& max_distance)
+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); by_surface[size_t(surface.surface_type)].emplace_back(&surface);
// Trim surfaces by the fill_boundaries. // Trim surfaces by the fill_boundaries.
this->fill_surfaces.surfaces.clear(); 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) { for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
const SurfacesPtr &this_surfaces = by_surface[surface_type]; const SurfacesPtr &this_surfaces = by_surface[surface_type];
if (! this_surfaces.empty()) 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 "Surface.hpp"
#include "libslic3r.h" #include "libslic3r.h"
#include <numeric>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <list> #include <list>
@@ -1361,10 +1362,15 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
} }
#endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM #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) { const size_t occluded_pairs = num_facets_states * layers.size();
for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) { 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()) { 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]); top_raw[extruder_idx][layer_idx] = diff(top_raw[extruder_idx][layer_idx], input_expolygons[layer_idx + 1]);
} }
@@ -1372,7 +1378,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]); 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); std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states);
@@ -1430,11 +1436,58 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
return out; 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, 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) { &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 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(); throw_on_cancel_callback();
LayerColorStat stat = layer_color_stat(layer_idx, color_idx); LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty()) if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
@@ -1443,18 +1496,10 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
top_ex = opening_ex(top_ex, stat.small_region_threshold); top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (! top_ex.empty()) { if (! top_ex.empty()) {
append(triangles_by_color_top[color_idx][layer_idx], top_ex); append(triangles_by_color_top[color_idx][layer_idx], top_ex);
float offset = 0.f; std::vector<size_t> shell_layers;
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)
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));
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]);
//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));
}
} }
} }
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty()) if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
@@ -1463,21 +1508,13 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold); bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
if (! bottom_ex.empty()) { if (! bottom_ex.empty()) {
append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex); append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex);
float offset = 0.f; std::vector<size_t> shell_layers;
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)
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);
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]);
//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));
}
} }
} }
} });
} }
}); });
@@ -1498,20 +1535,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) { &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) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
throw_on_cancel_callback(); throw_on_cancel_callback();
ExPolygons painted_exploys; // The per-colour unions below are independent of each other, so they run in parallel (a painted top or
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) { // 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]; 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_bottom[color_idx][layer_idx]));
append(self, std::move(triangles_by_color_top[color_idx][layer_idx])); append(self, std::move(triangles_by_color_top[color_idx][layer_idx]));
self = union_ex(self); 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); painted_exploys = union_ex(painted_exploys);
//BBS: merge the top and bottom shell layers //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 &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); auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys);
@@ -1520,7 +1560,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
append(self, top_area); append(self, top_area);
append(self, bottom_area); append(self, bottom_area);
self = union_ex(self); self = union_ex(self);
} });
// Trim one region by the other if some of the regions overlap. // Trim one region by the other if some of the regions overlap.
ExPolygons painted_regions; ExPolygons painted_regions;
for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) { for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) {
@@ -1887,7 +1927,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, std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers,
const size_t num_facets_states, const size_t num_facets_states,
const std::function<void()> &throw_on_cancel_callback) const std::function<void()> &throw_on_cancel_callback)
@@ -1898,33 +2000,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()); 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"; 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) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states); 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) { for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
throw_on_cancel_callback(); if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) {
if (!segmented_regions[layer_idx][extruder_id].empty()) { for (size_t bucket = 0; bucket < num_buckets; ++bucket)
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id]; append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
if (!top_and_bottom_layers.empty()) { continue;
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));
} }
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 }); // end of parallel_for
@@ -2212,16 +2372,56 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
assert(!color_poly.empty()); assert(!color_poly.empty());
assert(!color_poly.front().empty()); assert(!color_poly.front().empty());
if (has_layer_only_one_color(color_poly)) { // Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to
// 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. // that island's contours than to any other island's - the way out crosses its own boundary first - so its
segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx]; // Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief
} else { // has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions
MMU_Graph graph = build_graph(layer_idx, color_poly); // that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in
remove_multiple_edges_in_vertices(graph, color_poly); // parallel; an island in a single colour needs no diagram at all.
graph.remove_nodes_with_one_arc(); const ExPolygons &islands = input_expolygons[layer_idx];
segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states); std::vector<std::pair<size_t, size_t>> island_contours(islands.size()); // [first, last) into color_poly
//segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx); {
// 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 #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]); 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]);
@@ -2244,7 +2444,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
throw_on_cancel_callback(); 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(); throw_on_cancel_callback();
#ifdef MM_SEGMENTATION_DEBUG_REGIONS #ifdef MM_SEGMENTATION_DEBUG_REGIONS
+6 -2
View File
@@ -23,11 +23,15 @@ public:
MultiPoint() {} MultiPoint() {}
MultiPoint(const MultiPoint &other) : points(other.points) {} 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) {} MultiPoint(std::initializer_list<Point> list) : points(list) {}
explicit MultiPoint(const Points &_points) : points(_points) {} 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=(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; virtual ~MultiPoint() = default;
void scale(double factor); void scale(double factor);
void scale(double factor_x, double factor_y); void scale(double factor_x, double factor_y);
+424 -399
View File
@@ -32,6 +32,8 @@
#include <tuple> #include <tuple>
#include <unordered_set> #include <unordered_set>
#include <thread> #include <thread>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <vector> #include <vector>
#include "libslic3r.h" #include "libslic3r.h"
#include <utility> #include <utility>
@@ -2583,467 +2585,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); 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 // we need to process each island separately because we might have different
// extra perimeters for each one // extra perimeters for each one
for (const Surface& surface : all_surfaces) { // Each island is independent up to its outputs, so they are generated in parallel - a layer split into thousands
coord_t bead_width_0 = ext_perimeter_spacing; // of islands (e.g. by colour painting) otherwise ran on one thread - and the outputs are then committed in the
// detect how many perimeters must be generated for this island // original island order, which is what the extra overhang perimeters (applied to the last island's loops and to
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops // all fill surfaces so far) depend on.
int sparse_infill_density = this->config->sparse_infill_density.value; struct ArachneSurfaceResult
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall {
loop_number++; 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 // Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false; const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && only_one_wall_first_layer) if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0; loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config // Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false; const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && only_one_wall_top) if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0; loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter; 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. // 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), ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing ) apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.)); : -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config); 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. // 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; input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
coord_t wall_0_inset = 0; coord_t wall_0_inset = 0;
if (apply_precise_outer_wall) if (apply_precise_outer_wall)
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2); wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
//PS: One wall top surface for Arachne //PS: One wall top surface for Arachne
ExPolygons top_expolygons; ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected. // 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; const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected. // Set one perimeter when TopSurfaces is selected.
if (only_one_wall_top && loop_number > 0) if (only_one_wall_top && loop_number > 0)
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); Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1), Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
wall_0_inset, layer_height, input_params_tmp); wall_0_inset, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths(); std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour()); ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
// Check if there are some remaining perimeters to generate (the number of perimeters // 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). // is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) { if (inner_loop_number >= 0) {
assert(upper_slices != nullptr); 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. // 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" // 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); 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 // 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. // contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) { auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box. // Contour bounding box.
BoundingBox contour_bbox = get_extents(contour); BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON); contour_bbox.offset(SCALED_EPSILON);
Polygons upper_slices_clipped; Polygons upper_slices_clipped;
if (object_config->interface_shells) { if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces); 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); upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else } else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox); upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
ExPolygons top = diff_ex(contour, upper_slices_clipped); ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty(); uncovered = !top.empty();
if (top.empty()) if (top.empty())
return top; return top;
if (lower_slices != nullptr) { if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width)); 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 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); const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons. // Remove bridges from top surface polygons.
top = diff_ex(top, current_slices_bridges); 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. bool uncovered = false;
// ORCA: Expand the polygon with half the perimeter width in addition to the contracted amount, top_expolygons = get_top_expolygons(infill_contour, uncovered);
// 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). if (uncovered) {
return intersection_ex(top, contour); // 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. if (clip_walls_over_top) {
std::vector<Arachne::VariableWidthLines> full_perimeters; Polygons kept_over_top;
Polygons full_inner_contour; clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
bool full_perimeters_generated = false; // Route the top fill around the walls kept despite grazing the top.
auto generate_full_perimeters = [&]() { if (! kept_over_top.empty())
if (full_perimeters_generated) top_expolygons = diff_ex(top_expolygons, kept_over_top);
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 // Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
// widths by widening both, where the full pass adds a middle bead. Over the top surface that is the intent; if (!perimeters.empty())
// anywhere else it leaves no room for the inner walls. When the single wall pass's outer walls run away from for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
// the full pass's outside the top surface, take the full pass's outer walls and the area inside them instead. for (Arachne::ExtrusionLine &el : inner_perimeter)
// Walls closer than outer_wall_tolerance count as the same wall: a widened bead's centerline moves by half ++el.inset_idx;
// 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; perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
if (widest_bead(perimeters) > bead_width_0 + 2 * outer_wall_tolerance) { infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
// The single wall pass's inner contour where it widens no bead: inside nominal width outer walls. } else {
const ExPolygons nominal_infill_contour = offset_ex(last, -float(bead_width_0 + wall_0_inset)); // There is no top surface ExPolygon, so use the walls generated like when the single perimeter
bool nominal_uncovered = false; // feature is disabled.
// 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(); generate_full_perimeters();
if (! full_perimeters.empty() && ! full_perimeters.front().empty() && perimeters = std::move(full_perimeters);
length_off_reference(perimeters, full_perimeters.front(), top_zone, outer_wall_tolerance) > double(perimeter_width)) { infill_contour = union_ex(full_inner_contour);
perimeters = { full_perimeters.front() };
infill_contour = diff_ex(nominal_infill_contour, walls_footprint(full_perimeters.front()), ApplySafetyOffset::Yes);
}
} }
} }
//PS
bool uncovered = false; loop_number = int(perimeters.size()) - 1;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
if (uncovered) { #ifdef ARACHNE_DEBUG
// 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 static int iRun = 0;
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
// 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);
} }
} #endif
//PS
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 int start_perimeter = int(perimeters.size()) - 1;
{ int end_perimeter = -1;
static int iRun = 0; int direction = -1;
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
}
#endif
// All closed ExtrusionLine should have the same the first and the last point. bool is_outer_wall_first =
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without this->config->wall_sequence == WallSequence::OuterInner ||
// equal the first and the last point. this->config->wall_sequence == WallSequence::InnerOuterInner;
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;
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first = is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim(); this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
} }
if (is_outer_wall_first) { if (is_outer_wall_first) {
start_perimeter = 0; start_perimeter = 0;
end_perimeter = int(perimeters.size()); end_perimeter = int(perimeters.size());
direction = 1; 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);
} }
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool { std::vector<Arachne::ExtrusionLine*> all_extrusions;
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed; for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
}); if (perimeters[perimeter_idx].empty())
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; 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; std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
double distance_sqr = (current_position - candidate_position).cast<double>().norm(); return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
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; for (const size_t candidate_path_idx : available_candidates) {
best_distance_sqr = distance_sqr; auto& path = all_extrusions[candidate_path_idx];
is_best_closed = path->is_closed;
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]; // printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
ordered_extrusions.push_back({ best_path, best_path->is_contour() }); if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
processed[best_candidate] = true; if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
for (size_t unlocked_idx : blocking[best_candidate]) int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
blocked[unlocked_idx]--; 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. // 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
if (best_path->is_closed) // rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
current_position = best_path->junctions[0].p; //We end where we started. coord_t threshold_external = (apply_precise_outer_wall)
else // Precise outer wall: use the full external spacing
current_position = best_path->junctions.back().p; //Pick the other end from where we started. ? ( 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
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer + this->perimeter_flow.scaled_spacing()/2.0 );
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 the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
// for OI mode that is used the basis for IOI coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
bringContoursToFront(ordered_extrusions);
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
// Debug statement to print spacing values: // Re-order extrusions based on distance
//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()); // Alorithm will aggresively optimise for the appearance of the outermost perimeter
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
// 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 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.
// 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 );
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors. // Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
coord_t threshold_internal = this->perimeter_flow.scaled_spacing(); // 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
// Re-order extrusions based on distance // perimeters left.
// Alorithm will aggresively optimise for the appearance of the outermost perimeter while (position < reordered_extrusions.size()) {
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal ); outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
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. 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
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis // is found. Start at "position" index to enable the for loop to iterate for multiple external
// scan to find the external perimeter, first internal, second internal and last perimeter in the island. // perimeters in a single island
// We then advance the position index to move to the second island and continue until there are no more // 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);
// perimeters left. for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
while (position < reordered_extrusions.size()) { // printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1 switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list case 0: // external perimeter
// run through the walls to get the index values that need re-ordering until the first one for each if (outer == -1)
// is found. Start at "position" index to enable the for loop to iterate for multiple external outer = arr_i;
// perimeters in a single island break;
// 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); case 1: // first internal wall
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) { if (first_internal==-1 && arr_i>outer && outer!=-1){
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i); first_internal = arr_i;
switch (reordered_extrusions[arr_i].extrusion->inset_idx) { }
case 0: // external perimeter break;
if (outer == -1) case 2: // second internal wall
outer = arr_i; if (second_internal == -1 && arr_i > first_internal && outer!=-1){
break; second_internal = arr_i;
case 1: // first internal wall }
if (first_internal==-1 && arr_i>outer && outer!=-1){ break;
first_internal = arr_i; }
} 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.
break; // This means we entered a new island.
case 2: // second internal wall arr_i=arr_i-1; //step back one perimeter
if (second_internal == -1 && arr_i > first_internal && outer!=-1){ max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
second_internal = arr_i; break; // exit the for loop
}
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++;
} }
} }
// 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 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
// printf("Outside in loop: Mapped perimeter index %d to array position %d\n", arr_j, current_perimeter+1); if(arr_j >= second_internal){
inner_outer_extrusions[++current_perimeter] = reordered_extrusions[arr_j]; //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];
} }
// go to the next perimeter from the current position to continue scanning for external walls in the same island
for(arr_j = position; arr_j <= max_internal; ++arr_j) // replace perimeter array with the new re-ordered array position = arr_i + 1;
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;
} }
} }
}
bool steep_overhang_contour = false; bool steep_overhang_contour = false;
bool steep_overhang_hole = false; bool steep_overhang_hole = false;
if (!config->overhang_reverse) { if (!config->overhang_reverse) {
// Skip steep overhang detection no reverse is specified // Skip steep overhang detection no reverse is specified
steep_overhang_contour = true; steep_overhang_contour = true;
steep_overhang_hole = true; steep_overhang_hole = true;
} }
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) { if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
if (config->overhang_reverse) { if (config->overhang_reverse) {
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole, reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only); 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()) if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out. 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 // 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) // 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 // and then we offset back and forth by half the infill spacing to only consider the
// non-collapsing regions // non-collapsing regions
coord_t inset = coord_t inset =
(loop_number < 0) ? 0 : (loop_number < 0) ? 0 :
(loop_number == 0) ? (loop_number == 0) ?
// one loop // one loop
ext_perimeter_spacing : ext_perimeter_spacing :
// two or more loops? // two or more loops?
perimeter_spacing; perimeter_spacing;
coord_t top_inset = inset; coord_t top_inset = inset;
top_inset = coord_t(scale_(this->config->top_bottom_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) if(is_topmost_layer || is_bottom_layer)
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset)))); inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
else else
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset)))); inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
// simplify infill contours according to resolution // simplify infill contours according to resolution
Polygons pp; Polygons pp;
for (ExPolygon& ex : infill_contour) for (ExPolygon& ex : infill_contour)
ex.simplify_p(m_scaled_resolution, &pp); ex.simplify_p(m_scaled_resolution, &pp);
ExPolygons not_filled_exp = union_ex(pp); ExPolygons not_filled_exp = union_ex(pp);
// collapse too narrow infill areas // collapse too narrow infill areas
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE)); const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
ExPolygons infill_exp = offset2_ex( 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(
not_filled_exp, not_filled_exp,
float(-min_perimeter_infill_spacing / 2.), float(-min_perimeter_infill_spacing / 2.),
float(+min_perimeter_infill_spacing / 2.)); float(inset + min_perimeter_infill_spacing / 2.));
if (!top_expolygons.empty()) // append infill areas to fill_surfaces
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons); if (!top_expolygons.empty()) {
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end()); 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) {} explicit Polygon(const Points &points) : MultiPoint(points) {}
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {} Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
Polygon(const Polygon &other) : MultiPoint(other.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) { static Polygon new_scale(const std::vector<Vec2d> &points) {
Polygon pgn; Polygon pgn;
pgn.points.reserve(points.size()); pgn.points.reserve(points.size());
@@ -43,7 +43,7 @@ public:
return pgn; return pgn;
} }
Polygon& operator=(const Polygon &other) { points = other.points; return *this; } 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]; } Point& operator[](Points::size_type idx) { return this->points[idx]; }
const Point& operator[](Points::size_type idx) const { 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: public:
Polyline() {}; Polyline() {};
Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {} 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) { Polyline(std::initializer_list<Point> list) : MultiPoint(list) {
fitting_result.clear(); fitting_result.clear();
} }
@@ -47,7 +47,7 @@ public:
fitting_result = other.fitting_result; fitting_result = other.fitting_result;
return *this; return *this;
} }
Polyline& operator=(Polyline&& other) { Polyline& operator=(Polyline&& other) noexcept {
points = std::move(other.points); points = std::move(other.points);
fitting_result = std::move(other.fitting_result); fitting_result = std::move(other.fitting_result);
return *this; return *this;
+352 -184
View File
@@ -50,6 +50,7 @@
#include <cstdlib> #include <cstdlib>
#include <cstdint> #include <cstdint>
#include <float.h> #include <float.h>
#include <array>
#include <functional> #include <functional>
#include <ios> #include <ios>
#include <iomanip> #include <iomanip>
@@ -76,6 +77,7 @@
#include <Eigen/Core> #include <Eigen/Core>
#include <tbb/parallel_for.h> #include <tbb/parallel_for.h>
#include <tbb/parallel_invoke.h>
#include <tbb/spin_mutex.h> #include <tbb/spin_mutex.h>
#include <tbb/concurrent_unordered_set.h> #include <tbb/concurrent_unordered_set.h>
@@ -1930,7 +1932,9 @@ void PrintObject::detect_surfaces_type()
bool interface_shells = ! spiral_mode && m_config.interface_shells.value; 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(); 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"; BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start";
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
for (Layer *layer : m_layers) for (Layer *layer : m_layers)
@@ -1988,7 +1992,7 @@ void PrintObject::detect_surfaces_type()
if (upper_layer) { if (upper_layer) {
ExPolygons upper_slices = interface_shells ? 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->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); surfaces_append(top, opening_ex(upper_slices, offset), stTop);
} else { } else {
// if no upper layer, all surfaces of this one are solid // if no upper layer, all surfaces of this one are solid
@@ -2014,7 +2018,7 @@ void PrintObject::detect_surfaces_type()
surfaces_append( surfaces_append(
bottom, bottom,
opening_ex( 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), offset),
surface_type_bottom_other); surface_type_bottom_other);
// if user requested internal shells, we need to identify surfaces // if user requested internal shells, we need to identify surfaces
@@ -2045,34 +2049,44 @@ void PrintObject::detect_surfaces_type()
// and top surfaces; let's do an intersection to discover them and consider them // and top surfaces; let's do an intersection to discover them and consider them
// as bottom surfaces (to allow for bridge detection) // as bottom surfaces (to allow for bridge detection)
if (! top.empty() && ! bottom.empty()) { 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 (!cracks.empty()) {
if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom 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; 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) { for (const auto& crack : cracks) {
if (offset_ex(crack, small_crack_threshold).empty()) { 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 // 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; 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 && se.area() > crack.area() * 2
&& !offset_ex(diff_ex(se, crack), small_crack_threshold).empty(); && !offset_ex(diff_ex(se, crack), small_crack_threshold).empty();
})) continue; })) continue;
// Crack too small, leave it as part of the top surface, remove it from bottom surfaces // 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; Surfaces bot_tmp;
for (auto& b : bottom) { 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); bottom = std::move(bot_tmp);
} }
} }
} }
Polygons top_polygons = to_polygons(std::move(top)); ExPolygons top_expolygons = to_expolygons(std::move(top));
top.clear(); top.clear();
surfaces_append(top, diff_ex(top_polygons, bottom), stTop); surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop);
} }
} }
@@ -2163,7 +2177,7 @@ void PrintObject::detect_surfaces_type()
{ {
Polygons topbottom = to_polygons(top); Polygons topbottom = to_polygons(top);
polygons_append(topbottom, to_polygons(bottom)); 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)); surfaces_append(surfaces_out, std::move(top));
@@ -2340,29 +2354,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 ======================================================= // === 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"; 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. // Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
tbb::parallel_for( tbb::parallel_for(
@@ -2379,7 +2395,7 @@ void PrintObject::detect_surfaces_type()
}); });
m_print->throw_if_canceled(); m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end"; 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.) // 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; m_typed_slices = true;
@@ -2446,8 +2462,10 @@ void PrintObject::process_external_surfaces()
BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end"; 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 in parallel - start";
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " 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::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()), tbb::blocked_range<size_t>(0, m_layers.size()),
[this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) { [this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) {
@@ -2462,9 +2480,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() void PrintObject::discover_vertical_shells()
@@ -2503,10 +2521,10 @@ void PrintObject::discover_vertical_shells()
// The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit. // The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit.
return; return;
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom"; BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom";
//FIXME Improve the heuristics for a grain size. // One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a
size_t grain_size = std::max(num_layers / 16, size_t(1)); // few such layers next to each other must not end up in one task.
tbb::parallel_for( 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) { [this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) {
const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge }; const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge };
const size_t num_regions = this->num_printing_regions(); const size_t num_regions = this->num_printing_regions();
@@ -2514,67 +2532,198 @@ void PrintObject::discover_vertical_shells()
m_print->throw_if_canceled(); m_print->throw_if_canceled();
const Layer &layer = *m_layers[idx_layer]; const Layer &layer = *m_layers[idx_layer];
DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer]; DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer];
// Simulate single set of perimeters over all merged regions. const auto top_bottom_expansion = [&layer](size_t region_id) {
float perimeter_offset = 0.f; return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
float perimeter_min_spacing = FLT_MAX; };
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
static size_t debug_idx = 0; static size_t debug_idx = 0;
++ debug_idx; ++ debug_idx;
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
for (size_t region_id = 0; region_id < num_regions; ++ region_id) { // The top surfaces, the bottom surfaces and the holes are independent of each other.
LayerRegion &layerm = *layer.m_regions[region_id]; tbb::parallel_invoke(
float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff; [&]() {
// Top surfaces. for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion)); 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)); // append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Bottom surfaces. // Save some computing time by reducing the number of polygons.
append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion)); cache.top_surfaces = union_(cache.top_surfaces);
// 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. for (size_t region_id = 0; region_id < num_regions; ++ region_id)
unsigned int perimeters = 0; append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id)));
for (Surface &s : layerm.slices.surfaces) // append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters); cache.bottom_surfaces = union_(cache.bottom_surfaces);
perimeters += layerm.region().config().wall_loops.value; },
// Then calculate the infill offset. [&]() {
if (perimeters > 0) { // Simulate single set of perimeters over all merged regions.
Flow extflow = layerm.flow(frExternalPerimeter); float perimeter_offset = 0.f;
Flow flow = layerm.flow(frPerimeter); float perimeter_min_spacing = FLT_MAX;
perimeter_offset = std::max(perimeter_offset, for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing()); const LayerRegion &layerm = *layer.m_regions[region_id];
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing()))); // 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.
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons)); unsigned int perimeters = 0;
} for (const Surface &s : layerm.slices.surfaces)
// Save some computing time by reducing the number of polygons. perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
cache.top_surfaces = union_(cache.top_surfaces); perimeters += layerm.region().config().wall_loops.value;
cache.bottom_surfaces = union_(cache.bottom_surfaces); // Then calculate the infill offset.
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print. if (perimeters > 0) {
if (perimeter_offset > 0.) { Flow extflow = layerm.flow(frExternalPerimeter);
// The layer.lslices are forced to merge by expanding them first. Flow flow = layerm.flow(frPerimeter);
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing)); 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 #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{ {
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices)); 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(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red"); svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05)); svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close(); svg.Close();
} }
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
} }
cache.holes = union_(cache.holes); cache.holes = union_(cache.holes);
});
} }
}); });
m_print->throw_if_canceled(); m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom"; 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); const PrintRegion &region = this->printing_region(region_id);
if (region.config().ensure_vertical_shell_thickness.value != evstAll ) if (region.config().ensure_vertical_shell_thickness.value != evstAll )
// This region will be handled by discover_horizontal_shells(). // This region will be handled by discover_horizontal_shells().
continue; return;
//FIXME Improve the heuristics for a grain size. //FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1)); size_t grain_size = std::max(num_layers / 16, size_t(1));
@@ -2614,7 +2763,7 @@ void PrintObject::discover_vertical_shells()
grain_size = 1; grain_size = 1;
tbb::parallel_for( tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, grain_size), 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) { (const tbb::blocked_range<size_t>& range) {
// printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end()); // 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) { for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
@@ -2664,80 +2813,19 @@ void PrintObject::discover_vertical_shells()
} }
} }
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
polygons_append(holes, cache_top_botom_regions[idx_layer].holes); const AccumulationKey key = accumulation_key(region_config, layerm);
auto combine_holes = [&holes](const Polygons &holes2) { const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr :
if (holes.empty() || holes2.empty()) [&]() -> const ShellAccumulation * {
holes.clear(); for (const ShellAccumulation &a : shell_accumulations[idx_layer])
else if (a.key == key)
holes = intersection(holes, holes2); return &a;
}; return nullptr;
auto combine_shells = [&shell](const Polygons &shells2) { }();
if (shell.empty()) if (reused != nullptr) {
shell = std::move(shells2); shell = reused->shell;
else if (! shells2.empty()) { holes = reused->holes;
polygons_append(shell, shells2); } else
// Running the union_ using the Clipper library piece by piece is cheaper accumulate_shell(idx_layer, region_config, layerm, shell, holes);
// 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);
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{ {
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell)); Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
@@ -2831,11 +2919,8 @@ void PrintObject::discover_vertical_shells()
Polygons object_volume; Polygons object_volume;
Polygons internal_volume; Polygons internal_volume;
{ {
Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{}; if (idx_layer > 0 && idx_layer + 1 < m_layers.size())
Polygons shrinked_upper_slice = (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)));
to_polygons(m_layers[idx_layer + 1]->lslices) :
Polygons{};
object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice);
internal_volume = closing(polygonsInternal, SCALED_EPSILON); internal_volume = closing(polygonsInternal, SCALED_EPSILON);
} }
@@ -2846,15 +2931,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 // 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 // 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 // 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(), 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) { &object_volume](const ExPolygon &p) {
return (p.area() < min_perimeter_infill_spacing * scaled(1.5) || const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) && (p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p), object_volume).empty())) && diff(to_polygons(p),
diff(internal_volume, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
expand(to_polygons(p), min_perimeter_infill_spacing)) object_volume, get_extents(p).inflated(SCALED_EPSILON)))
.size() >= internal_volume.size(); .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()); regularized_shell.end());
} }
@@ -2876,8 +2980,9 @@ void PrintObject::discover_vertical_shells()
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
// Trim the internal & internalvoid by the shell. // Trim the internal & internalvoid by the shell.
Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell); const Polygons regularized_shell_polygons = to_polygons(regularized_shell);
Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), 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 #ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{ {
@@ -2904,7 +3009,15 @@ void PrintObject::discover_vertical_shells()
layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final"); layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final");
} }
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */ #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() } // void PrintObject::discover_vertical_shells()
// #define DEBUG_BRIDGE_OVER_INFILL // #define DEBUG_BRIDGE_OVER_INFILL
@@ -3425,6 +3538,16 @@ void PrintObject::bridge_over_infill()
vertical_lines[i].b = Point{x, y_max}; 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 anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)};
auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)}; auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)};
@@ -3669,26 +3792,58 @@ void PrintObject::bridge_over_infill()
std::vector<CandidateSurface> expanded_surfaces; std::vector<CandidateSurface> expanded_surfaces;
expanded_surfaces.reserve(surfaces_by_layer[lidx].size()); 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]) { for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
const auto &region_config = candidate.region->region().config(); const auto &region_config = candidate.region->region().config();
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve || const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
region_config.sparse_infill_pattern == ipOctagramSpiral; region_config.sparse_infill_pattern == ipOctagramSpiral;
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true); 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. // Orca: Filter whole bridge areas so their holes remain holes.
bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(), bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(),
[&internal_unsupported_area](const ExPolygon &component) { [&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()); bridge_components.end());
Polygons area_to_be_bridge = to_polygons(std::move(bridge_components)); 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()) if (area_to_be_bridge.empty())
continue; 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())); 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()); boundary_plines.insert(boundary_plines.end(), limiting_plines.begin(), limiting_plines.end());
@@ -3764,9 +3919,12 @@ void PrintObject::bridge_over_infill()
// Check collision with other expanded surfaces // Check collision with other expanded surfaces
{ {
bool reconstruct = false; 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) { 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; bridging_angle = s.bridge_angle;
reconstruct = true; reconstruct = true;
break; break;
@@ -3790,10 +3948,20 @@ void PrintObject::bridge_over_infill()
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow, bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
bridging_angle, scan_spacing, true)); bridging_angle, scan_spacing, true));
} }
bridging_area = intersection(bridging_area, limiting_area); // Each of these meets one bridge with the whole layer, so the layer side is first cut down to the
bridging_area = intersection(bridging_area, total_fill_area); // bridge's box (and expansion_area split as above); the result is the same.
bridging_area = diff(bridging_area, total_top_area); if (!bridging_area.empty()) {
expansion_area = diff(expansion_area, bridging_area); 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 #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), 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); ::fread(&y, sizeof(coord_t), 1, file);
poly.points.emplace_back(Point(x * scale, y * scale)); poly.points.emplace_back(Point(x * scale, y * scale));
} }
printf("Polygon %d, area: %lf\n", i, area(poly.points));
if (which == -1 || which == i) if (which == -1 || which == i)
m_support_polygons_deserialized.emplace_back(std::move(poly)); 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); ::fread(&n_polygons, 4, 1, file);
m_trimming_polygons_deserialized.reserve(n_polygons); 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) if (is_auto(stype) && config_detect_sharp_tails)
{ {
// BBS detect sharp tail // 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) { for (const ExPolygon& expoly : curr_polys) {
bool is_sharp_tail = false; bool is_sharp_tail = false;
// 1. nothing below // 1. nothing below
// this is a sharp tail region if it's floating and non-ignorable. // 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 const ExPolygons expanded = offset_ex(expoly, 0.1 * extrusion_width_scaled);
// shear-advanced lower layer, or every belt-contact island reads as const BoundingBox bbox = get_extents(expanded);
// a sharp tail -- which is what the empty-predecessor skip above was ExPolygons lower_nearby;
// really masking. effective_lower is exactly that notion of below. for (size_t i = 0; i < tail_lower.size(); ++i)
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys; if (lower_bboxes[i].overlap(bbox))
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) { 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(); 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), thickness(other.thickness), thickness_layers(other.thickness_layers),
bridge_angle(other.bridge_angle), extra_perimeters(other.extra_perimeters) 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)), : surface_type(rhs.surface_type), expolygon(std::move(rhs.expolygon)),
thickness(rhs.thickness), thickness_layers(rhs.thickness_layers), thickness(rhs.thickness), thickness_layers(rhs.thickness_layers),
bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters) bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters)
@@ -94,7 +94,7 @@ public:
return *this; return *this;
} }
Surface& operator=(Surface &&rhs) Surface& operator=(Surface &&rhs) noexcept
{ {
surface_type = rhs.surface_type; surface_type = rhs.surface_type;
expolygon = std::move(rhs.expolygon); expolygon = std::move(rhs.expolygon);
+4 -4
View File
@@ -167,10 +167,10 @@ inline void append(std::vector<T, Alloc> &dest, std::vector<T, Alloc> &&src)
{ {
if (dest.empty()) if (dest.empty())
dest = std::move(src); dest = std::move(src);
else { else
dest.reserve(dest.size() + src.size()); // insert() grows the capacity geometrically; reserving exactly the new size reallocated on every call, which
std::move(std::begin(src), std::end(src), std::back_inserter(dest)); // 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.clear();
src.shrink_to_fit(); src.shrink_to_fit();
} }
-62
View File
@@ -707,10 +707,6 @@ set(SLIC3R_GUI_SOURCES
plugin/PluginManager.hpp plugin/PluginManager.hpp
plugin/PluginAuditManager.cpp plugin/PluginAuditManager.cpp
plugin/PluginAuditManager.hpp plugin/PluginAuditManager.hpp
plugin/PluginAuditManagerGui.cpp
plugin/PluginAuditManagerInternal.hpp
plugin/PluginModule.cpp
plugin/PluginText.hpp
plugin/PluginResolver.cpp plugin/PluginResolver.cpp
plugin/PluginResolver.hpp plugin/PluginResolver.hpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.hpp
@@ -1096,61 +1092,3 @@ if (ORCA_BUILD_PYTHON_STUBGEN_MODULE)
PREFIX "" PREFIX ""
) )
endif() endif()
# Headless plugin permission probe. A distinct executable so normal GUI loading and prompt
# behavior stay untouched. It compiles the GUI-free plugin sources directly instead of linking
# libslic3r_gui, so neither the GUI library nor wx is pulled into the probe. The permission
# front end is selected by source composition: PluginAuditManagerGui.cpp for the GUI app and
# probe/PluginAuditManagerHeadless.cpp here.
#
# Residual cost: the probe still links libslic3r (the large core library) for Utils,
# LifecycleEvents, the model/print types used by the GUI-free host bindings and libslic3r's own
# transitive dependencies. It does not link libslic3r_gui, wx or the GUI-only host/plugin sources.
# Developer/security-analysis tool, so it is off by default.
if (ORCA_BUILD_PLUGIN_PERMISSION_PROBE)
add_executable(orca-plugin-probe
plugin/PluginPermissionProbe.cpp
plugin/PluginProbeCollector.hpp
plugin/probe/NanoSvgImplementation.cpp
plugin/PluginAuditManager.cpp
plugin/PluginAuditManagerInternal.hpp
plugin/PluginModule.cpp
plugin/PluginText.hpp
plugin/probe/PluginAuditManagerHeadless.cpp
plugin/PluginFsUtils.cpp
plugin/PluginLoader.cpp
plugin/PythonInterpreter.cpp
plugin/PythonPluginBridge.cpp
plugin/probe/PluginHostProbe.cpp
plugin/probe/PluginProbeConfig.cpp
plugin/host/PluginHostGeometry.cpp
plugin/host/PluginHostMesh.cpp
plugin/host/PluginHostModel.cpp
plugin/host/PluginHostSlicing.cpp
plugin/pluginTypes/printerAgent/PrinterAgentPluginCapability.cpp
plugin/pluginTypes/pages/PagesPluginCapability.cpp
plugin/pluginTypes/resourceTexture/ResourceTexturePluginCapability.cpp
plugin/pluginTypes/script/ScriptPluginCapability.cpp
plugin/pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.cpp
)
target_compile_definitions(orca-plugin-probe PRIVATE ORCA_PLUGIN_PROBE_BUILD)
target_include_directories(orca-plugin-probe PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}
${CMAKE_CURRENT_BINARY_DIR}
${CMAKE_SOURCE_DIR}/src
Utils
)
target_link_libraries(orca-plugin-probe PRIVATE
libslic3r
nanosvg
pybind11::embed
)
set_target_properties(orca-plugin-probe PROPERTIES
OUTPUT_NAME orca-plugin-probe
FOLDER "tools"
)
endif()
+320 -111
View File
@@ -1,5 +1,4 @@
#include "PluginAuditManager.hpp" #include "PluginAuditManager.hpp"
#include "PluginAuditManagerInternal.hpp"
#include "../Utils/OrcaCloudServiceAgent.hpp" #include "../Utils/OrcaCloudServiceAgent.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
@@ -16,12 +15,25 @@
#include <cctype> #include <cctype>
#include <cstddef> #include <cstddef>
#include <cstdlib> #include <cstdlib>
#include <future>
#include <mutex> #include <mutex>
#include <memory>
#include <slic3r/GUI/BindDialog.hpp>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <wx/app.h>
#include <wx/event.h>
#include <wx/msgdlg.h>
#include <wx/string.h>
#include <wx/thread.h>
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/plugin/PluginDescriptor.hpp"
#include <Python.h> #include <Python.h>
namespace Slic3r { namespace Slic3r {
@@ -149,10 +161,8 @@ static bool is_fs_category(AuditEventCategory category)
} }
} }
namespace plugin_audit {
// The category's name in a saved event grant, e.g. "FsRead" in "FsRead:open". // The category's name in a saved event grant, e.g. "FsRead" in "FsRead:open".
const char* category_name(AuditEventCategory category) static const char* category_name(AuditEventCategory category)
{ {
switch (category) { switch (category) {
case AuditEventCategory::FsRead: return "FsRead"; case AuditEventCategory::FsRead: return "FsRead";
@@ -169,29 +179,6 @@ const char* category_name(AuditEventCategory category)
return "None"; return "None";
} }
int report_denied(PluginAuditManager& mgr,
const std::string& event_name,
const AuditDecision& decision,
const std::string& target)
{
AuditViolation violation;
violation.plugin_key = mgr.current_plugin();
violation.event_name = event_name;
violation.reason = decision.reason;
mgr.report_violation(violation);
std::string message = "Plugin attempted audited operation \"" + event_name + "\" without permission";
if (!decision.reason.empty())
message += ": " + decision.reason;
if (!target.empty())
message += ": " + target;
PyErr_SetString(PyExc_PermissionError, message.c_str());
return -1;
}
} // namespace plugin_audit
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Path safety // Path safety
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@@ -260,7 +247,6 @@ bool is_inside_allowed_root(const boost::filesystem::path& candidate, const boos
thread_local std::string PluginAuditManager::m_current_plugin_key = ""; thread_local std::string PluginAuditManager::m_current_plugin_key = "";
thread_local std::string PluginAuditManager::m_current_capability_name = ""; thread_local std::string PluginAuditManager::m_current_capability_name = "";
thread_local std::string PluginAuditManager::m_current_audit_phase = "";
thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots; thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
thread_local std::vector<PyObject*> PluginAuditManager::m_root_frames; thread_local std::vector<PyObject*> PluginAuditManager::m_root_frames;
thread_local int PluginAuditManager::m_releasing_frames = 0; thread_local int PluginAuditManager::m_releasing_frames = 0;
@@ -330,76 +316,6 @@ std::string PluginAuditManager::current_capability() const { return m_current_ca
void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); } void PluginAuditManager::clear_current_capability() { m_current_capability_name.clear(); }
void PluginAuditManager::set_current_audit_phase(const std::string& phase) { m_current_audit_phase = phase; }
const std::string& PluginAuditManager::current_audit_phase() const { return m_current_audit_phase; }
void PluginAuditManager::clear_current_audit_phase() { m_current_audit_phase.clear(); }
void PluginAuditManager::set_observation_observer(AuditObserver observer)
{
std::lock_guard<std::mutex> lock(m_observer_mutex);
m_observation_observer = std::move(observer);
}
void PluginAuditManager::clear_observation_observer()
{
std::lock_guard<std::mutex> lock(m_observer_mutex);
m_observation_observer = nullptr;
}
bool PluginAuditManager::observation_mode() const
{
std::lock_guard<std::mutex> lock(m_observer_mutex);
return static_cast<bool>(m_observation_observer);
}
// The audit hook holds the GIL here. Copy the observer out before invoking it so a callback that
// reads the manager cannot deadlock on m_observer_mutex.
bool PluginAuditManager::record_observation(const std::string& event_name,
const std::string& category,
const std::vector<std::string>& targets)
{
AuditObserver observer;
{
std::lock_guard<std::mutex> lock(m_observer_mutex);
observer = m_observation_observer;
}
if (!observer)
return false;
// The observer is called from a CPython C callback; a C++ exception must not unwind through
// the audit hook. The probe's collector only allocates, so a throw here is a lost observation,
// not a reason to abort the audited operation.
try {
AuditObservation observation;
observation.plugin_key = m_current_plugin_key;
observation.capability = m_current_capability_name;
observation.phase = m_current_audit_phase;
observation.event_name = event_name;
observation.category = category;
observation.targets = targets;
// Probe mode never denies: the hook returns 0 after recording. A later failure of the
// surrounding Python operation is reported by the probe's callback/load result, not here.
observation.continued = true;
observer(observation);
} catch (...) {
}
return true;
}
ScopedPluginAuditPhase::ScopedPluginAuditPhase(std::string phase)
: m_phase(std::move(phase))
, m_previous_phase(PluginAuditManager::instance().current_audit_phase())
{
PluginAuditManager::instance().set_current_audit_phase(m_phase);
}
ScopedPluginAuditPhase::~ScopedPluginAuditPhase()
{
PluginAuditManager::instance().set_current_audit_phase(m_previous_phase);
}
void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write) void PluginAuditManager::add_global_allowed_root(const boost::filesystem::path& root, bool allow_write)
{ {
if (root.empty()) if (root.empty())
@@ -620,14 +536,73 @@ bool PluginAuditManager::request_filesystem_read_permissions(const std::string&
if (plugin_key.empty() || paths.empty()) if (plugin_key.empty() || paths.empty())
return true; return true;
// Probe mode records the declaration and proceeds. It must not consult the install state, PluginDescriptor descriptor;
// open the permission dialog, or persist a grant. if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
if (observation_mode()) { return false;
record_observation("orca.request_permissions", "FsRead", paths);
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
std::vector<std::string> missing;
for (const std::string& path : paths) {
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
missing.push_back(path);
}
if (missing.empty())
return true; return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
auto show_dialog = [&descriptor, &missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
_L("Plugin permissions"),
wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
};
bool granted = false;
if (wxIsMainThread()) {
granted = show_dialog();
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
_L("Plugin permissions"),
wxYES_NO | wxICON_WARNING);
result->set_value(dialog.ShowModal() == wxID_YES);
});
granted = future.get();
} }
return plugin_audit::request_filesystem_read_permissions(plugin_key, paths); if (!granted)
return false;
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
} }
void PluginAuditManager::report_violation(const AuditViolation& violation) void PluginAuditManager::report_violation(const AuditViolation& violation)
@@ -889,18 +864,239 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
return targets; return targets;
} }
// The saved list that grants an event: its category's target list, or the event-grant list for
// an event with no target. Kept apart, so that no grant doubles as a target of the same name (an
// os.fork grant is not an os.exec of a command named "os.fork").
std::vector<std::string>* permission_list_for(AuditEventCategory category, bool by_event, PluginPermissions& permissions)
{
if (by_event)
return &permissions.events;
switch (category) {
case AuditEventCategory::FsRead: return &permissions.fs_read;
case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite;
case AuditEventCategory::Http: return &permissions.network_http;
case AuditEventCategory::Socket: return &permissions.network_socket;
case AuditEventCategory::ProcessCreate: return &permissions.process;
case AuditEventCategory::Threading: return &permissions.threading;
case AuditEventCategory::ProcessReplace: return &permissions.process;
default: return nullptr;
}
}
// An event with no target is granted by its effective category and name, e.g. "FsRead:open"
// for a read of a file descriptor, which does not grant a write of one ("FsReadWrite:open").
std::string event_grant_key(AuditEventCategory category, const std::string& event_name)
{
return std::string(category_name(category)) + ":" + event_name;
}
bool has_permission(const std::vector<std::string>& granted, const std::string& target)
{
return std::find(granted.begin(), granted.end(), target) != granted.end();
}
bool persist_permission(const std::string& plugin_key,
PluginInstallState& state,
std::vector<std::string>& permission_list,
const std::string& target)
{
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
// needed by the writer so clicking Yes still creates the JSON file.
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
if (!has_permission(permission_list, target))
permission_list.push_back(target);
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
int report_denied(PluginAuditManager& mgr,
const std::string& event_name,
const AuditDecision& decision,
const std::string& target = {})
{
AuditViolation violation;
violation.plugin_key = mgr.current_plugin();
violation.event_name = event_name;
violation.reason = decision.reason;
mgr.report_violation(violation);
std::string message = "Plugin attempted audited operation \"" + event_name + "\" without permission";
if (!decision.reason.empty())
message += ": " + decision.reason;
if (!target.empty())
message += ": " + target;
PyErr_SetString(PyExc_PermissionError, message.c_str());
return -1;
}
wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
const wxString& target_list, size_t target_count)
{
if (target_list.IsEmpty())
return wxString::Format(
_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n"
"This operation does not expose a target the audit hook can display."),
plugin_name, event_name);
switch (category) {
case AuditEventCategory::FsRead:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to read the following file:\n%s",
"Plugin \"%s\" is requesting to read the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::FsReadWrite:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to read/write the following file:\n%s",
"Plugin \"%s\" is requesting to read/write the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::FsCreate:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to create the following file:\n%s",
"Plugin \"%s\" is requesting to create the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::FsDelete:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to delete the following file:\n%s",
"Plugin \"%s\" is requesting to delete the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::Http:
return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list);
case AuditEventCategory::Socket:
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessCreate:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to run the following command:\n%s",
"Plugin \"%s\" is requesting to run the following commands:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::Threading:
return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name);
case AuditEventCategory::ProcessReplace:
return wxString::Format(_L("Plugin \"%s\" is requesting to replace the running application with:\n%s"), plugin_name, target_list);
default:
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
}
}
// Builds and shows the modal permission prompt. Must run on the GUI thread.
bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name,
const std::vector<std::string>& unresolved)
{
wxString target_list;
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list, unresolved.size()),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
}
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
// freshly because the async prompt outlives the caller's stack copy of it.
void persist_grant(const std::string& plugin_key, AuditEventCategory category, bool by_event, const std::vector<std::string>& keys)
{
PluginInstallState state;
if (!PluginManager::instance().get_install_state(plugin_key, state))
return;
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
if (!permission_list)
return;
for (const auto& key : keys)
persist_permission(plugin_key, state, *permission_list, key);
}
static PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
// no target (by_event), which the prompt does not show as a target.
//
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
// re-prompting.
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
{
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
if (s_permission_prompt_for_testing)
return s_permission_prompt_for_testing(event_name, shown);
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
if (wxIsMainThread())
return prompt_for_targets(category, plugin_name, event_name, shown);
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, by_event, keys, shown]() {
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return;
if (prompt_for_targets(category, plugin_name, event_name, shown))
persist_grant(plugin_key, category, by_event, keys);
});
return false;
}
// Decides an event that no allowed root or running operation covers: a saved grant allows it,
// otherwise the user is asked and a Yes is saved.
int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state,
const std::string& plugin_key,
const std::string& plugin_name,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets)
{
const bool by_event = targets.empty();
const std::vector<std::string> keys = by_event ? std::vector<std::string>{event_grant_key(category, event_name)} : targets;
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
std::vector<std::string> unresolved = keys;
if (permission_list) {
unresolved.clear();
for (const auto& key : keys)
if (!has_permission(*permission_list, key))
unresolved.push_back(key);
if (unresolved.empty())
return 0;
}
// Letting a root frame go runs finalizers inside a scope destructor or another event's hook,
// where no dialog may open: what they do must already be granted.
if (mgr.releasing_frames())
return report_denied(mgr, event_name, {false, "audit permission required"});
if (!request_permission(category, plugin_key, plugin_name, event_name, by_event, unresolved))
return report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list)
for (const auto& key : unresolved)
persist_permission(plugin_key, state, *permission_list, key);
return 0;
}
// Judges an audited event raised while a plugin context is active. // Judges an audited event raised while a plugin context is active.
int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, AuditEventCategory event_type, PyObject* args) int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, AuditEventCategory event_type, PyObject* args)
{ {
const bool fs_category = is_fs_category(event_type); const bool fs_category = is_fs_category(event_type);
const std::vector<std::string> targets = audit_targets(event_name, event_type, args); const std::vector<std::string> targets = audit_targets(event_name, event_type, args);
// Probe mode: report the classified event and let it through. Deliberately before the deny
// registry and allowed-root checks, so the report covers operations that production would
// block as well as those it would allow. No grant is read or written.
if (mgr.record_observation(event_name, plugin_audit::category_name(event_type), targets))
return 0;
// A denied path (secrets, certificates, config files -- see is_denied_path) is an // A denied path (secrets, certificates, config files -- see is_denied_path) is an
// unconditional block: checked before the running-operation check below, so no granted // unconditional block: checked before the running-operation check below, so no granted
// operation can launder access to one, and before the allowed-root shortcut further down, // operation can launder access to one, and before the allowed-root shortcut further down,
@@ -909,7 +1105,7 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
if (fs_category) { if (fs_category) {
for (const auto& target : targets) { for (const auto& target : targets) {
if (mgr.is_denied_path(boost::filesystem::path(target))) if (mgr.is_denied_path(boost::filesystem::path(target)))
return plugin_audit::report_denied(mgr, event_name, {false, "denied path"}, target); return report_denied(mgr, event_name, {false, "denied path"}, target);
} }
} }
@@ -934,7 +1130,15 @@ int audit_plugin_event(PluginAuditManager& mgr, const std::string& event_name, A
return 0; return 0;
} }
const int result = plugin_audit::decide_event(mgr, event_name, event_type, targets); PluginInstallState state;
const bool have_install_state = PluginManager::instance().get_install_state(mgr.current_plugin(), state);
if (!have_install_state) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr.current_plugin();
}
const std::string plugin_name = state.plugin_name.empty() ? mgr.current_plugin() : state.plugin_name;
const int result = decide_audited_event(mgr, state, mgr.current_plugin(), plugin_name, event_name, event_type, targets);
// A granted operation covers its nested events while it runs. A targetless grant names no // A granted operation covers its nested events while it runs. A targetless grant names no
// operation, so it covers nothing else. // operation, so it covers nothing else.
@@ -1021,6 +1225,11 @@ void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
PyErr_SetRaisedException(error); PyErr_SetRaisedException(error);
} }
void PluginAuditManager::set_permission_prompt_for_testing(PermissionPrompt prompt)
{
PluginAuditDetail::s_permission_prompt_for_testing = std::move(prompt);
}
int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data) int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user_data)
{ {
auto* mgr = static_cast<PluginAuditManager*>(user_data); auto* mgr = static_cast<PluginAuditManager*>(user_data);
-59
View File
@@ -24,19 +24,6 @@ struct AuditViolation {
std::string reason; std::string reason;
}; };
// One classified CPython audit event observed while the probe is in record-only mode. This is
// evidence about the code path that ran, not a grant request: the hook allowed the operation to
// continue and did not consult the permission dialog or the persisted install state.
struct AuditObservation {
std::string plugin_key;
std::string capability;
std::string phase;
std::string event_name;
std::string category;
std::vector<std::string> targets;
bool continued = true;
};
// A filesystem root a plugin may access while an audit context is active. allow_write is // A filesystem root a plugin may access while an audit context is active. allow_write is
// false for a root that only grants reads (e.g. the bundled, shared resources folder) -- // false for a root that only grants reads (e.g. the bundled, shared resources folder) --
// a write-shaped event never matches such a root, even though a read-shaped one does. // a write-shaped event never matches such a root, even though a read-shaped one does.
@@ -150,30 +137,6 @@ public:
bool request_filesystem_read_permissions(const std::string& plugin_key, bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths); const std::vector<std::string>& paths);
// --- probe observation mode (thread-safe, never used by the GUI) ---
// When an observer is installed, every classified event raised by plugin-authored code is
// reported to it and then allowed through (the hook returns 0). The permission dialogs, saved
// grants and .install_state.json are never consulted, so this is not a containment boundary.
// The observer must not touch Python objects: it runs inside the audit hook with the GIL held.
using AuditObserver = std::function<void(const AuditObservation&)>;
void set_observation_observer(AuditObserver observer);
void clear_observation_observer();
bool observation_mode() const;
// Stop after classification/target extraction; true means an observer consumed the event.
// Exposed for the probe and its tests, not for production callers.
bool record_observation(const std::string& event_name,
const std::string& category,
const std::vector<std::string>& targets);
// --- probe phase attribution (thread_local) ---
// Labels events for the probe's report. Production leaves it empty. A Python thread spawned by
// plugin code inherits no phase, so its events are reported as unattributed rather than being
// guessed at.
void set_current_audit_phase(const std::string& phase);
const std::string& current_audit_phase() const;
void clear_current_audit_phase();
void report_violation(const AuditViolation& violation); void report_violation(const AuditViolation& violation);
bool audit_denial_pending() const; bool audit_denial_pending() const;
void clear_audit_denial(); void clear_audit_denial();
@@ -215,7 +178,6 @@ private:
static thread_local std::string m_current_plugin_key; static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name; static thread_local std::string m_current_capability_name;
static thread_local std::string m_current_audit_phase;
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots; static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
static thread_local std::vector<PyObject*> m_root_frames; // strong references static thread_local std::vector<PyObject*> m_root_frames; // strong references
static thread_local int m_releasing_frames; static thread_local int m_releasing_frames;
@@ -228,11 +190,6 @@ private:
std::vector<AllowedRoot> m_global_allowed_roots; std::vector<AllowedRoot> m_global_allowed_roots;
std::vector<std::string> m_denied_filenames; std::vector<std::string> m_denied_filenames;
std::vector<std::string> m_denied_path_keywords; std::vector<std::string> m_denied_path_keywords;
// Kept apart from m_mutex: observation_mode() is queried from the audit hook while the hook
// already holds the GIL, and the setter runs before plugin code starts.
mutable std::mutex m_observer_mutex;
AuditObserver m_observation_observer;
}; };
// RAII guard that sets the current plugin key and capability name, restoring the previous // RAII guard that sets the current plugin key and capability name, restoring the previous
@@ -263,22 +220,6 @@ private:
std::vector<PyObject*> m_previous_root_frames; std::vector<PyObject*> m_previous_root_frames;
}; };
// Sets the probe's phase label on the current thread and restores the previous one on scope exit.
// Empty in production, where no phase is ever set.
class ScopedPluginAuditPhase
{
public:
explicit ScopedPluginAuditPhase(std::string phase);
~ScopedPluginAuditPhase();
ScopedPluginAuditPhase(const ScopedPluginAuditPhase&) = delete;
ScopedPluginAuditPhase& operator=(const ScopedPluginAuditPhase&) = delete;
private:
std::string m_phase;
std::string m_previous_phase;
};
} // namespace Slic3r } // namespace Slic3r
#endif // slic3r_PluginAuditManager_hpp_ #endif // slic3r_PluginAuditManager_hpp_
-332
View File
@@ -1,332 +0,0 @@
#include "PluginAuditManagerInternal.hpp"
#include "PluginDescriptor.hpp"
#include "PluginFsUtils.hpp"
#include "PluginManager.hpp"
#include <boost/filesystem.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <cstddef>
#include <future>
#include <memory>
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <string>
#include <utility>
#include <vector>
#include <slic3r/GUI/GUI_App.hpp>
#include <slic3r/GUI/I18N.hpp>
#include <wx/app.h>
#include <wx/msgdlg.h>
#include <wx/string.h>
#include <wx/thread.h>
namespace Slic3r {
namespace {
// The saved list that grants an event: its category's target list, or the event-grant list for
// an event with no target. Kept apart, so that no grant doubles as a target of the same name (an
// os.fork grant is not an os.exec of a command named "os.fork").
std::vector<std::string>* permission_list_for(AuditEventCategory category, bool by_event, PluginPermissions& permissions)
{
if (by_event)
return &permissions.events;
switch (category) {
case AuditEventCategory::FsRead: return &permissions.fs_read;
case AuditEventCategory::FsReadWrite: return &permissions.fs_readwrite;
case AuditEventCategory::Http: return &permissions.network_http;
case AuditEventCategory::Socket: return &permissions.network_socket;
case AuditEventCategory::ProcessCreate: return &permissions.process;
case AuditEventCategory::Threading: return &permissions.threading;
case AuditEventCategory::ProcessReplace: return &permissions.process;
default: return nullptr;
}
}
// An event with no target is granted by its effective category and name, e.g. "FsRead:open"
// for a read of a file descriptor, which does not grant a write of one ("FsReadWrite:open").
std::string event_grant_key(AuditEventCategory category, const std::string& event_name)
{
return std::string(plugin_audit::category_name(category)) + ":" + event_name;
}
bool has_permission(const std::vector<std::string>& granted, const std::string& target)
{
return std::find(granted.begin(), granted.end(), target) != granted.end();
}
bool persist_permission(const std::string& plugin_key,
PluginInstallState& state,
std::vector<std::string>& permission_list,
const std::string& target)
{
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
// A sandbox plugin may not have a sidecar yet. Seed the small amount of install metadata
// needed by the writer so clicking Yes still creates the JSON file.
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
if (!has_permission(permission_list, target))
permission_list.push_back(target);
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
wxString audit_message(AuditEventCategory category, const wxString& plugin_name, const wxString& event_name,
const wxString& target_list, size_t target_count)
{
if (target_list.IsEmpty())
return wxString::Format(
_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\".\n\n"
"This operation does not expose a target the audit hook can display."),
plugin_name, event_name);
switch (category) {
case AuditEventCategory::FsRead:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to read the following file:\n%s",
"Plugin \"%s\" is requesting to read the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::FsReadWrite:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to read/write the following file:\n%s",
"Plugin \"%s\" is requesting to read/write the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::FsCreate:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to create the following file:\n%s",
"Plugin \"%s\" is requesting to create the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::FsDelete:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to delete the following file:\n%s",
"Plugin \"%s\" is requesting to delete the following files:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::Http:
return wxString::Format(_L("Plugin \"%s\" is requesting to make an HTTP request to:\n%s"), plugin_name, target_list);
case AuditEventCategory::Socket:
return wxString::Format(_L("Plugin \"%s\" is requesting to open a network connection to:\n%s"), plugin_name, target_list);
case AuditEventCategory::ProcessCreate:
return wxString::Format(_L_PLURAL("Plugin \"%s\" is requesting to run the following command:\n%s",
"Plugin \"%s\" is requesting to run the following commands:\n%s", target_count),
plugin_name, target_list);
case AuditEventCategory::Threading:
return wxString::Format(_L("Plugin \"%s\" is requesting permission to create a thread."), plugin_name);
case AuditEventCategory::ProcessReplace:
return wxString::Format(_L("Plugin \"%s\" is requesting to replace the running application with:\n%s"), plugin_name, target_list);
default:
return wxString::Format(_L("Plugin \"%s\" is requesting permission for the Python audit event \"%s\"."), plugin_name, event_name);
}
}
// Builds and shows the modal permission prompt. Must run on the GUI thread.
bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_name, const std::string& event_name,
const std::vector<std::string>& unresolved)
{
wxString target_list;
for (const auto& target : unresolved)
target_list += wxString::FromUTF8(target.c_str()) + "\n";
wxMessageDialog dialog(nullptr,
audit_message(category, wxString::FromUTF8(plugin_name.c_str()),
wxString::FromUTF8(event_name.c_str()), target_list, unresolved.size()),
_L("Plugin permission request"), wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
}
// Records a grant in the plugin's sidecar so it is not asked again. Reads the install state
// freshly because the async prompt outlives the caller's stack copy of it.
void persist_grant(const std::string& plugin_key, AuditEventCategory category, bool by_event, const std::vector<std::string>& keys)
{
PluginInstallState state;
if (!PluginManager::instance().get_install_state(plugin_key, state))
return;
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
if (!permission_list)
return;
for (const auto& key : keys)
persist_permission(plugin_key, state, *permission_list, key);
}
PluginAuditManager::PermissionPrompt s_permission_prompt_for_testing;
// Asks the user to grant an audited event, returning true when they approve an inline prompt.
// `keys` are what a Yes saves: the unresolved targets, or the event-grant key when the event has
// no target (by_event), which the prompt does not show as a target.
//
// An audited event can fire on a thread the UI thread may itself be blocked waiting on: the
// SlicingPipeline hook runs on the slicing worker thread (see PluginHooks.cpp), and
// BackgroundSlicingProcess::stop()/stop_internal() park the UI thread until that worker stops.
// Blocking the worker on a marshaled modal -- which is safe for the plugin-load worker that
// request_filesystem_read_permissions runs on -- would therefore deadlock the application (the
// invariant PluginHostUi.cpp documents for slicing-hook UI calls). Off the main thread the prompt
// is therefore posted asynchronously and the current event denied (fail closed, like an unanswered
// prompt); the grant is persisted once the user accepts, so a later attempt succeeds without
// re-prompting.
bool request_permission(AuditEventCategory category, const std::string& plugin_key, const std::string& plugin_name,
const std::string& event_name, bool by_event, const std::vector<std::string>& keys)
{
const std::vector<std::string> shown = by_event ? std::vector<std::string>{} : keys;
if (s_permission_prompt_for_testing)
return s_permission_prompt_for_testing(event_name, shown);
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
if (wxIsMainThread())
return prompt_for_targets(category, plugin_name, event_name, shown);
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, by_event, keys, shown]() {
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return;
if (prompt_for_targets(category, plugin_name, event_name, shown))
persist_grant(plugin_key, category, by_event, keys);
});
return false;
}
// Decides an event that no allowed root or running operation covers: a saved grant allows it,
// otherwise the user is asked and a Yes is saved.
int decide_audited_event(PluginAuditManager& mgr,
PluginInstallState& state,
const std::string& plugin_key,
const std::string& plugin_name,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets)
{
const bool by_event = targets.empty();
const std::vector<std::string> keys = by_event ? std::vector<std::string>{event_grant_key(category, event_name)} : targets;
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
std::vector<std::string> unresolved = keys;
if (permission_list) {
unresolved.clear();
for (const auto& key : keys)
if (!has_permission(*permission_list, key))
unresolved.push_back(key);
if (unresolved.empty())
return 0;
}
// Letting a root frame go runs finalizers inside a scope destructor or another event's hook,
// where no dialog may open: what they do must already be granted.
if (mgr.releasing_frames())
return plugin_audit::report_denied(mgr, event_name, {false, "audit permission required"});
if (!request_permission(category, plugin_key, plugin_name, event_name, by_event, unresolved))
return plugin_audit::report_denied(mgr, event_name, {false, "audit permission required"});
if (permission_list)
for (const auto& key : unresolved)
persist_permission(plugin_key, state, *permission_list, key);
return 0;
}
} // namespace
bool plugin_audit::request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths)
{
PluginDescriptor descriptor;
if (!PluginManager::instance().try_get_plugin_descriptor(plugin_key, descriptor) || descriptor.plugin_root.empty())
return false;
PluginInstallState state;
read_install_state(boost::filesystem::path(descriptor.plugin_root), state);
std::vector<std::string> missing;
for (const std::string& path : paths) {
if (std::find(state.permissions.fs_read.begin(), state.permissions.fs_read.end(), path) == state.permissions.fs_read.end())
missing.push_back(path);
}
if (missing.empty())
return true;
if (wxTheApp == nullptr || GUI::wxGetApp().is_closing())
return false;
auto show_dialog = [&descriptor, &missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
wxString::FromUTF8(descriptor.name.c_str()), requested_paths),
_L("Plugin permissions"),
wxYES_NO | wxICON_WARNING);
return dialog.ShowModal() == wxID_YES;
};
bool granted = false;
if (wxIsMainThread()) {
granted = show_dialog();
} else {
auto result = std::make_shared<std::promise<bool>>();
auto future = result->get_future();
GUI::wxGetApp().CallAfter([result, descriptor_name = descriptor.name, missing]() {
wxString requested_paths;
for (const std::string& path : missing)
requested_paths += wxString::FromUTF8(path.c_str()) + "\n";
wxMessageDialog dialog(
nullptr,
wxString::Format(_L("Plugin \"%s\" requests filesystem read access to:\n%s"),
wxString::FromUTF8(descriptor_name.c_str()), requested_paths),
_L("Plugin permissions"),
wxYES_NO | wxICON_WARNING);
result->set_value(dialog.ShowModal() == wxID_YES);
});
granted = future.get();
}
if (!granted)
return false;
if (state.plugin_name.empty()) {
state.installed_from = descriptor.is_cloud_plugin() ? "cloud" : "local";
state.installed_version = !descriptor.installed_version.empty() ? descriptor.installed_version : descriptor.version;
state.plugin_name = descriptor.name;
state.cloud_uuid = descriptor.cloud_uuid();
state.enabled = true;
}
state.permissions.fs_read.insert(state.permissions.fs_read.end(), missing.begin(), missing.end());
return write_install_state(boost::filesystem::path(descriptor.plugin_root), state);
}
int plugin_audit::decide_event(PluginAuditManager& mgr,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets)
{
PluginInstallState state;
const bool have_install_state = PluginManager::instance().get_install_state(mgr.current_plugin(), state);
if (!have_install_state) {
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << " Failed to get install state for " << mgr.current_plugin();
}
const std::string plugin_name = state.plugin_name.empty() ? mgr.current_plugin() : state.plugin_name;
return decide_audited_event(mgr, state, mgr.current_plugin(), plugin_name, event_name, category, targets);
}
void PluginAuditManager::set_permission_prompt_for_testing(PermissionPrompt prompt)
{
s_permission_prompt_for_testing = std::move(prompt);
}
} // namespace Slic3r
@@ -1,38 +0,0 @@
#ifndef slic3r_PluginAuditManagerInternal_hpp_
#define slic3r_PluginAuditManagerInternal_hpp_
// Internal seam between the GUI-free audit core and the permission front end. The GUI library
// supplies the dialogs, prompts and .install_state.json persistence; the headless probe supplies
// a fail-closed implementation that never links wx. Production behavior lives in the GUI
// implementation, which is the code that used to be part of PluginAuditManager.cpp.
#include "PluginAuditManager.hpp"
#include <string>
#include <vector>
namespace Slic3r::plugin_audit {
const char* category_name(AuditEventCategory category);
int report_denied(PluginAuditManager& mgr,
const std::string& event_name,
const AuditDecision& decision,
const std::string& target = {});
// Used only when the manager is not in observation mode. The GUI implementation consults the
// install state, prompts and persists; the probe implementation fails closed.
bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths);
// Decides an event that no allowed root or running operation covered. The GUI implementation
// reads the install state and prompts; the probe implementation fails closed. In observation
// mode the audit hook records and returns before reaching this seam.
int decide_event(PluginAuditManager& mgr,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets);
} // namespace Slic3r::plugin_audit
#endif // slic3r_PluginAuditManagerInternal_hpp_
+17 -16
View File
@@ -14,7 +14,8 @@
#include "PluginAuditManager.hpp" #include "PluginAuditManager.hpp"
#include "PythonInterpreter.hpp" #include "PythonInterpreter.hpp"
#include "PluginText.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp"
#include <exception> #include <exception>
#include <miniz.h> #include <miniz.h>
@@ -91,7 +92,7 @@ bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
const fs::path plugin_root = resolve_plugin_root_from_descriptor(descriptor); const fs::path plugin_root = resolve_plugin_root_from_descriptor(descriptor);
if (plugin_root.empty()) { if (plugin_root.empty()) {
error = PLUGIN_U8L("Plugin folder could not be determined."); error = _u8L("Plugin folder could not be determined.");
return false; return false;
} }
@@ -102,7 +103,7 @@ bool resolve_allowed_plugin_root(const PluginDescriptor& descriptor,
resolved_root = fs::absolute(plugin_root, ec); resolved_root = fs::absolute(plugin_root, ec);
} }
if (ec || resolved_root.empty()) { if (ec || resolved_root.empty()) {
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to resolve plugin folder: %1%"), plugin_root.string()); error = GUI::format(_u8L("Failed to resolve plugin folder: %1%"), plugin_root.string());
return false; return false;
} }
@@ -122,7 +123,7 @@ bool delete_plugin_root(const boost::filesystem::path& resolved_root, const std:
const auto removed_count = fs::remove_all(resolved_root, ec); const auto removed_count = fs::remove_all(resolved_root, ec);
if (ec) { if (ec) {
// TRN %1% is a folder path, %2% the system error message. // TRN %1% is a folder path, %2% the system error message.
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to delete plugin folder %1%: %2%"), resolved_root.string(), ec.message()); error = GUI::format(_u8L("Failed to delete plugin folder %1%: %2%"), resolved_root.string(), ec.message());
return false; return false;
} }
@@ -1198,7 +1199,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
namespace fs = boost::filesystem; namespace fs = boost::filesystem;
if (!fs::exists(plugin_dir) || !fs::is_directory(plugin_dir)) { if (!fs::exists(plugin_dir) || !fs::is_directory(plugin_dir)) {
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin directory does not exist: %1%"), plugin_dir.string()); error = GUI::format(_u8L("Plugin directory does not exist: %1%"), plugin_dir.string());
return {}; return {};
} }
@@ -1215,7 +1216,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
if (ext == ".py") { if (ext == ".py") {
if (!py_entry.empty()) { if (!py_entry.empty()) {
// TRN %1% and %2% are file names. // TRN %1% and %2% are file names.
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin directory contains multiple .py files: %1% and %2%"), error = GUI::format(_u8L("Plugin directory contains multiple .py files: %1% and %2%"),
py_entry.filename().string(), it->path().filename().string()); py_entry.filename().string(), it->path().filename().string());
return {}; return {};
} }
@@ -1223,7 +1224,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
} else if (ext == ".whl") { } else if (ext == ".whl") {
if (!whl_entry.empty()) { if (!whl_entry.empty()) {
// TRN %1% and %2% are file names. // TRN %1% and %2% are file names.
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin directory contains multiple .whl files: %1% and %2%"), error = GUI::format(_u8L("Plugin directory contains multiple .whl files: %1% and %2%"),
whl_entry.filename().string(), it->path().filename().string()); whl_entry.filename().string(), it->path().filename().string());
return {}; return {};
} }
@@ -1232,7 +1233,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
} }
if (!py_entry.empty() && !whl_entry.empty()) { if (!py_entry.empty() && !whl_entry.empty()) {
error = PLUGIN_U8L("Plugin directory contains both .py and .whl entry files"); error = _u8L("Plugin directory contains both .py and .whl entry files");
return {}; return {};
} }
@@ -1242,7 +1243,7 @@ boost::filesystem::path find_installed_plugin_entry(const boost::filesystem::pat
if (!whl_entry.empty()) if (!whl_entry.empty())
return whl_entry; return whl_entry;
error = PLUGIN_U8L("Plugin directory does not contain a .py or .whl entry file"); error = _u8L("Plugin directory does not contain a .py or .whl entry file");
return {}; return {};
} }
@@ -1259,34 +1260,34 @@ std::vector<std::string> collect_resource_texture_files(const std::string& dir,
error.clear(); error.clear();
if (dir.empty()) { if (dir.empty()) {
error = PLUGIN_U8L("Resource texture directory is not set"); error = _u8L("Resource texture directory is not set");
return files; return files;
} }
if (plugin_root.empty()) { if (plugin_root.empty()) {
error = PLUGIN_U8L("Plugin folder could not be determined."); error = _u8L("Plugin folder could not be determined.");
return files; return files;
} }
boost::system::error_code ec; boost::system::error_code ec;
const fs::path resolved_dir = fs::weakly_canonical(fs::path(dir), ec); const fs::path resolved_dir = fs::weakly_canonical(fs::path(dir), ec);
if (ec) { if (ec) {
error = PLUGIN_U8L("Resource texture directory could not be resolved"); error = _u8L("Resource texture directory could not be resolved");
return files; return files;
} }
if (!fs::is_directory(resolved_dir, ec) || ec) { if (!fs::is_directory(resolved_dir, ec) || ec) {
error = PLUGIN_U8L("Resource texture path is not a directory"); error = _u8L("Resource texture path is not a directory");
return files; return files;
} }
const fs::path resolved_root = fs::weakly_canonical(fs::path(plugin_root), ec); const fs::path resolved_root = fs::weakly_canonical(fs::path(plugin_root), ec);
if (ec) { if (ec) {
error = PLUGIN_U8L("Plugin folder could not be resolved"); error = _u8L("Plugin folder could not be resolved");
return files; return files;
} }
if (!is_inside_allowed_root(resolved_dir, resolved_root)) { if (!is_inside_allowed_root(resolved_dir, resolved_root)) {
error = PLUGIN_U8L("Resource texture directory is outside the plugin folder"); error = _u8L("Resource texture directory is outside the plugin folder");
return files; return files;
} }
@@ -1311,7 +1312,7 @@ std::vector<std::string> collect_resource_texture_files(const std::string& dir,
} }
if (ec) { if (ec) {
error = PLUGIN_U8L("Resource texture directory could not be read"); error = _u8L("Resource texture directory could not be read");
return {}; return {};
} }
+25 -24
View File
@@ -11,7 +11,8 @@
#include "PythonPluginBridge.hpp" #include "PythonPluginBridge.hpp"
#include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp" #include "pluginTypes/resourceTexture/ResourceTexturePluginCapability.hpp"
#include "libslic3r/Utils.hpp" #include "libslic3r/Utils.hpp"
#include "PluginText.hpp" #include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp"
#include <boost/algorithm/string.hpp> #include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/case_conv.hpp> #include <boost/algorithm/string/case_conv.hpp>
@@ -101,7 +102,7 @@ bool read_local_plugin_package_metadata(const boost::filesystem::path& source_pa
const std::string ext = plugin_package_extension(source_path); const std::string ext = plugin_package_extension(source_path);
if (ext != ".py" && ext != ".whl") { if (ext != ".py" && ext != ".whl") {
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin package must be a .py or .whl file, got: %1%"), ext); error = GUI::format(_u8L("Plugin package must be a .py or .whl file, got: %1%"), ext);
return false; return false;
} }
@@ -178,14 +179,14 @@ bool add_wheel_dependencies_to_sys_path(const PluginDescriptor& descriptor,
std::string extract_error; std::string extract_error;
if (!extract_zip_to_directory(it->path(), dep_dir, extract_error)) { if (!extract_zip_to_directory(it->path(), dep_dir, extract_error)) {
// TRN %1% is the dependency file, %2% the error detail. // TRN %1% is the dependency file, %2% the error detail.
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to extract plugin .whl dependency %1%: %2%"), it->path().string(), extract_error); error = GUI::format(_u8L("Failed to extract plugin .whl dependency %1%: %2%"), it->path().string(), extract_error);
return false; return false;
} }
} }
std::string syspath_error; std::string syspath_error;
if (!interpreter.add_plugin_sys_path(dep_dir.string(), syspath_error)) { if (!interpreter.add_plugin_sys_path(dep_dir.string(), syspath_error)) {
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to add .whl dependency to sys.path: %1%"), syspath_error); error = GUI::format(_u8L("Failed to add .whl dependency to sys.path: %1%"), syspath_error);
return false; return false;
} }
plugin_paths.push_back(dep_dir.string()); plugin_paths.push_back(dep_dir.string());
@@ -247,12 +248,12 @@ bool load(const PluginDescriptor& descriptor,
PythonInterpreter& interpreter = PythonInterpreter::instance(); PythonInterpreter& interpreter = PythonInterpreter::instance();
if (!interpreter.is_initialized()) { if (!interpreter.is_initialized()) {
error = PLUGIN_FORMAT(PLUGIN_U8L("Python interpreter not initialized: %1%"), interpreter.last_error()); error = GUI::format(_u8L("Python interpreter not initialized: %1%"), interpreter.last_error());
return false; return false;
} }
if (!descriptor.is_metadata_valid()) { if (!descriptor.is_metadata_valid()) {
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin manifest is invalid: %1%"), descriptor.plugin_key); error = GUI::format(_u8L("Plugin manifest is invalid: %1%"), descriptor.plugin_key);
if (descriptor.has_error()) if (descriptor.has_error())
error += " - " + descriptor.normalized_error(); error += " - " + descriptor.normalized_error();
return false; return false;
@@ -273,7 +274,7 @@ bool load(const PluginDescriptor& descriptor,
if (!skip_deps) { if (!skip_deps) {
std::string pkg_install_error; std::string pkg_install_error;
if (!install_packages(descriptor.dependencies, pkg_install_error)) { if (!install_packages(descriptor.dependencies, pkg_install_error)) {
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to install plugin dependencies: %1%"), pkg_install_error); error = GUI::format(_u8L("Failed to install plugin dependencies: %1%"), pkg_install_error);
return false; return false;
} }
} }
@@ -292,7 +293,7 @@ bool load(const PluginDescriptor& descriptor,
PyObject* module = import_plugin_module(descriptor, plugin.plugin_sys_paths, plugin.plugin_modules, load_error); PyObject* module = import_plugin_module(descriptor, plugin.plugin_sys_paths, plugin.plugin_modules, load_error);
if (module == nullptr) { if (module == nullptr) {
bridge.cancel_plugin_capture(descriptor.entry_path); bridge.cancel_plugin_capture(descriptor.entry_path);
error = PLUGIN_FORMAT(PLUGIN_U8L("Failed to load plugin module: %1%"), load_error); error = GUI::format(_u8L("Failed to load plugin module: %1%"), load_error);
return false; return false;
} }
@@ -307,13 +308,13 @@ bool load(const PluginDescriptor& descriptor,
auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error); auto capabilities_found = bridge.finalize_plugin_capture(descriptor.entry_path, descriptor.plugin_key, bridge_error);
if (!bridge_error.empty()) { if (!bridge_error.empty()) {
capabilities_found.clear(); capabilities_found.clear();
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin registration failed: %1%"), bridge_error); error = GUI::format(_u8L("Plugin registration failed: %1%"), bridge_error);
if (registration_failed != nullptr) if (registration_failed != nullptr)
*registration_failed = true; *registration_failed = true;
return false; return false;
} }
if (capabilities_found.empty()) { if (capabilities_found.empty()) {
error = PLUGIN_U8L("Plugin module did not register any capabilities"); error = _u8L("Plugin module did not register any capabilities");
return false; return false;
} }
@@ -365,7 +366,7 @@ bool load(const PluginDescriptor& descriptor,
if (!seen_capabilities[type].insert(found.name).second) { if (!seen_capabilities[type].insert(found.name).second) {
// TRN %1% is the capability name, %2% the capability type (e.g. Script). // TRN %1% is the capability name, %2% the capability type (e.g. Script).
materialization_error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin declares duplicate capability '%1%' for type %2%"), materialization_error = GUI::format(_u8L("Plugin declares duplicate capability '%1%' for type %2%"),
found.name, PluginConfig::capability_type_label(type)); found.name, PluginConfig::capability_type_label(type));
break; break;
} }
@@ -421,9 +422,9 @@ bool load(const PluginDescriptor& descriptor,
capabilities.push_back(std::move(instance)); capabilities.push_back(std::move(instance));
} }
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
materialization_error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin capability materialization failed: %1%"), ex.what()); materialization_error = GUI::format(_u8L("Plugin capability materialization failed: %1%"), ex.what());
} catch (...) { } catch (...) {
materialization_error = PLUGIN_U8L("Plugin capability materialization failed"); materialization_error = _u8L("Plugin capability materialization failed");
} }
} }
@@ -465,11 +466,11 @@ bool load(const PluginDescriptor& descriptor,
} catch (const std::exception& ex) { } catch (const std::exception& ex) {
teardown_capabilities(plugin.capabilities, lifecycle_count); teardown_capabilities(plugin.capabilities, lifecycle_count);
// TRN on_load is the name of the plugin's startup function; keep it untranslated. // TRN on_load is the name of the plugin's startup function; keep it untranslated.
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin on_load failed: %1%"), ex.what()); error = GUI::format(_u8L("Plugin on_load failed: %1%"), ex.what());
return false; return false;
} catch (...) { } catch (...) {
teardown_capabilities(plugin.capabilities, lifecycle_count); teardown_capabilities(plugin.capabilities, lifecycle_count);
error = PLUGIN_U8L("Plugin on_load failed"); error = _u8L("Plugin on_load failed");
return false; return false;
} }
@@ -650,7 +651,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
bool backup_created = false; bool backup_created = false;
auto fail = [&error](const std::string& message) { auto fail = [&error](const std::string& message) {
error = PLUGIN_FORMAT(PLUGIN_U8L("Plugin installation failed: %1%"), message); error = GUI::format(_u8L("Plugin installation failed: %1%"), message);
return false; return false;
}; };
@@ -658,23 +659,23 @@ bool install_plugin(const boost::filesystem::path& filepath,
const fs::path source_path = canonical_or_absolute(filepath); const fs::path source_path = canonical_or_absolute(filepath);
if (!fs::exists(source_path, ec) || !fs::is_regular_file(source_path, ec)) if (!fs::exists(source_path, ec) || !fs::is_regular_file(source_path, ec))
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Plugin package is not a file: %1%"), filepath.string())); return fail(GUI::format(_u8L("Plugin package is not a file: %1%"), filepath.string()));
const std::string ext = plugin_package_extension(source_path); const std::string ext = plugin_package_extension(source_path);
if (ext != ".py" && ext != ".whl") if (ext != ".py" && ext != ".whl")
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Plugin package must be a .py or .whl file, got: %1%"), ext)); return fail(GUI::format(_u8L("Plugin package must be a .py or .whl file, got: %1%"), ext));
// The cloud UUID is concatenated into the install path (final_dir = plugin_root / cloud_uuid). // The cloud UUID is concatenated into the install path (final_dir = plugin_root / cloud_uuid).
// It comes verbatim from the server's catalog JSON, so reject anything that could escape the // It comes verbatim from the server's catalog JSON, so reject anything that could escape the
// per-user plugin root (traversal, leading dots, separators) before touching the filesystem. // per-user plugin root (traversal, leading dots, separators) before touching the filesystem.
if (is_cloud_install && !is_valid_plugin_id(cloud_uuid)) if (is_cloud_install && !is_valid_plugin_id(cloud_uuid))
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Cloud plugin UUID is not a valid identifier: %1%"), cloud_uuid)); return fail(GUI::format(_u8L("Cloud plugin UUID is not a valid identifier: %1%"), cloud_uuid));
const fs::path plugin_root = is_cloud_install && !cloud_user_id.empty() ? fs::path(get_cloud_plugin_dir(cloud_user_id)) : const fs::path plugin_root = is_cloud_install && !cloud_user_id.empty() ? fs::path(get_cloud_plugin_dir(cloud_user_id)) :
local_plugin_root(); local_plugin_root();
fs::create_directories(plugin_root, ec); fs::create_directories(plugin_root, ec);
if (ec) if (ec)
// TRN %1% is a folder path, %2% the system error message. // TRN %1% is a folder path, %2% the system error message.
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to create plugin directory %1%: %2%"), plugin_root.string(), ec.message())); return fail(GUI::format(_u8L("Failed to create plugin directory %1%: %2%"), plugin_root.string(), ec.message()));
std::string meta_error; std::string meta_error;
if (is_cloud_install) { if (is_cloud_install) {
@@ -717,7 +718,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
fs::rename(final_dir, backup_dir, ec); fs::rename(final_dir, backup_dir, ec);
if (ec) if (ec)
// TRN %1% is a folder path, %2% the system error message. // TRN %1% is a folder path, %2% the system error message.
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to back up existing plugin %1%: %2%"), final_dir.string(), ec.message())); return fail(GUI::format(_u8L("Failed to back up existing plugin %1%: %2%"), final_dir.string(), ec.message()));
backup_created = true; backup_created = true;
} }
@@ -728,7 +729,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
boost::system::error_code restore_ec; boost::system::error_code restore_ec;
fs::rename(backup_dir, final_dir, restore_ec); fs::rename(backup_dir, final_dir, restore_ec);
} }
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to create plugin directory %1%: %2%"), final_dir.string(), ec.message())); return fail(GUI::format(_u8L("Failed to create plugin directory %1%: %2%"), final_dir.string(), ec.message()));
} }
fs::copy_file(source_path, dest_file, ec); fs::copy_file(source_path, dest_file, ec);
@@ -739,7 +740,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
fs::rename(backup_dir, final_dir, restore_ec); fs::rename(backup_dir, final_dir, restore_ec);
} }
// TRN %1% is a file path, %2% the system error message. // TRN %1% is a file path, %2% the system error message.
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to copy plugin file to %1%: %2%"), dest_file.string(), ec.message())); return fail(GUI::format(_u8L("Failed to copy plugin file to %1%: %2%"), dest_file.string(), ec.message()));
} }
// Update entry_path to the installed location. // Update entry_path to the installed location.
@@ -759,7 +760,7 @@ bool install_plugin(const boost::filesystem::path& filepath,
fs::remove_all(final_dir, rollback_ec); fs::remove_all(final_dir, rollback_ec);
if (backup_created) if (backup_created)
fs::rename(backup_dir, final_dir, rollback_ec); fs::rename(backup_dir, final_dir, rollback_ec);
return fail(PLUGIN_FORMAT(PLUGIN_U8L("Failed to write plugin install state: %1%"), (final_dir / INSTALL_STATE_FILE).string())); return fail(GUI::format(_u8L("Failed to write plugin install state: %1%"), (final_dir / INSTALL_STATE_FILE).string()));
} }
} }
+12
View File
@@ -53,6 +53,18 @@ const char* const LOAD_CANCELLED = "__plugin_load_cancelled__";
} // namespace } // namespace
void Plugin::release_module()
{
if (module == nullptr && plugin_sys_paths.empty() && plugin_modules.empty())
return;
PythonInterpreter::instance().unload_module(module, module_name, plugin_sys_paths, plugin_modules);
module = nullptr;
module_name.clear();
plugin_sys_paths.clear();
plugin_modules.clear();
}
PluginManager& PluginManager::instance() PluginManager& PluginManager::instance()
{ {
// Touch the interpreter first so its static outlives this one: ~Plugin needs it alive to // Touch the interpreter first so its static outlives this one: ~Plugin needs it alive to
-22
View File
@@ -1,22 +0,0 @@
// Plugin module ownership is part of the loader's contract, not of the registry's GUI-facing
// surface. Kept in its own translation unit so the headless probe can link the loader without
// pulling PluginManager.cpp (and therefore wx/GUI) into the probe.
#include "PluginManager.hpp"
#include "PythonInterpreter.hpp"
namespace Slic3r {
void Plugin::release_module()
{
if (module == nullptr && plugin_sys_paths.empty() && plugin_modules.empty())
return;
PythonInterpreter::instance().unload_module(module, module_name, plugin_sys_paths, plugin_modules);
module = nullptr;
module_name.clear();
plugin_sys_paths.clear();
plugin_modules.clear();
}
} // namespace Slic3r
-565
View File
@@ -1,565 +0,0 @@
// Headless, record-only plugin permission probe.
//
// This is a developer/security-analysis tool, not a second production loader. It reuses the
// embedded interpreter, the production plugin_loader sequence and the GUI-free `orca` bindings
// (capability types plus the geometry/model/slicing host domains), so the observations come from
// the same audit classification and target extraction as the GUI. It then lets every audited
// operation continue instead of consulting permission grants or dialogs. Host domains that need
// the live GUI (app, presets, ui) and plugin.storage are registered as explicit unsupported
// errors; the config API reports an empty config and refuses writes.
//
// It is NOT a sandbox. Plugin code runs with the probe's filesystem, network and process
// privileges. The temporary data directory only keeps OrcaSlicer's own state out of the user's
// tree; it does not confine plugin-authored code. Use OS-level restrictions (container, user,
// seccomp/AppArmor, firewall) when the package is not trusted.
//
// The report describes only the paths this run exercised. CPython audit events are observations of
// particular operations, not a notification for every Python call, so absence of a category in the
// report is not evidence that the plugin cannot perform it.
#include "PluginAuditManager.hpp"
#include "PluginDescriptor.hpp"
#include "PluginFsUtils.hpp"
#include "PluginLoader.hpp"
#include "PluginManager.hpp"
#include "PluginProbeCollector.hpp"
#include "PythonInterpreter.hpp"
#include "PythonPluginInterface.hpp"
#include "libslic3r/LifecycleEvents.hpp"
#include "libslic3r/Utils.hpp"
#include <boost/algorithm/string/case_conv.hpp>
#include <boost/filesystem.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/system/error_code.hpp>
#include <nlohmann/json.hpp>
#include <pybind11/embed.h>
#include <pybind11/gil.h>
#include <pybind11/pytypes.h>
#include <cstdlib>
#include <exception>
#include <iostream>
#include <memory>
#include <string>
#include <utility>
#include <vector>
namespace fs = boost::filesystem;
namespace py = pybind11;
using namespace Slic3r;
namespace {
constexpr const char* kLoadPhase = "load:import+registration+on_load";
struct Options
{
fs::path plugin_path;
bool json = false;
};
void print_usage(std::ostream& out)
{
out << "Usage: orca-plugin-probe [--json] <plugin.py|plugin.whl>\n"
"\n"
"Runs a plugin in a temporary data/working directory and reports the CPython audit\n"
"events observed while its import, registration, on_load and discoverable callbacks\n"
"execute. The probe does not prompt for permissions, write grants, install dependencies,\n"
"or start the OrcaSlicer GUI.\n"
"\n"
" --json Emit the report as JSON (default: human-readable text)\n"
" --help Show this message\n"
"\n"
"WARNING: this is not a sandbox. Plugin Python code runs with this process's privileges.\n"
"Results are observational and non-exhaustive. Use OS-level restrictions for untrusted\n"
"packages.\n"
"\n"
"Exit status: 0 plugin loaded, 1 initialization/load failure, 2 usage error.\n";
}
bool parse_options(int argc, char** argv, Options& options, std::string& error)
{
for (int index = 1; index < argc; ++index) {
const std::string argument = argv[index];
if (argument == "--help" || argument == "-h") {
print_usage(std::cout);
std::exit(0);
}
if (argument == "--json") {
options.json = true;
continue;
}
if (!argument.empty() && argument.front() == '-') {
error = "Unknown option: " + argument;
return false;
}
if (!options.plugin_path.empty()) {
error = "Only one plugin package may be probed at a time.";
return false;
}
options.plugin_path = fs::path(argument);
}
if (options.plugin_path.empty()) {
error = "No plugin package given.";
return false;
}
return true;
}
// Keep every write OrcaSlicer makes during the run inside a throwaway tree, and keep relative
// paths opened by plugin code out of the caller's working directory. This is hygiene, not
// confinement: an absolute path supplied by the plugin is still opened normally.
class ScopedProbeWorkspace
{
public:
ScopedProbeWorkspace()
: m_root(fs::temp_directory_path() / fs::unique_path("orca-plugin-probe-%%%%-%%%%-%%%%"))
, m_previous_data_dir(data_dir())
, m_previous_working_dir(fs::current_path())
{
fs::create_directories(m_root);
set_data_dir(m_root.string());
fs::current_path(m_root);
m_moved = true;
}
~ScopedProbeWorkspace()
{
boost::system::error_code ec;
set_data_dir(m_previous_data_dir);
if (m_moved)
fs::current_path(m_previous_working_dir, ec);
fs::remove_all(m_root, ec);
}
const fs::path& root() const { return m_root; }
private:
fs::path m_root;
std::string m_previous_data_dir;
fs::path m_previous_working_dir;
bool m_moved = false;
};
class ScopedObservationObserver
{
public:
explicit ScopedObservationObserver(PluginProbeCollector& collector) : m_collector(collector)
{
PluginAuditManager::instance().set_observation_observer(
[this](const AuditObservation& observation) { m_collector.record(observation); });
}
~ScopedObservationObserver() { PluginAuditManager::instance().clear_observation_observer(); }
ScopedObservationObserver(const ScopedObservationObserver&) = delete;
ScopedObservationObserver& operator=(const ScopedObservationObserver&) = delete;
private:
PluginProbeCollector& m_collector;
};
bool prepare_descriptor(const fs::path& plugin_path, PluginDescriptor& descriptor, std::string& error)
{
boost::system::error_code ec;
const fs::path canonical = fs::weakly_canonical(plugin_path, ec);
if (ec || !fs::exists(canonical) || !fs::is_regular_file(canonical)) {
error = "Plugin package is not a readable file: " + plugin_path.string();
return false;
}
std::string extension = canonical.extension().string();
boost::algorithm::to_lower(extension);
if (extension != ".py" && extension != ".whl") {
error = "Plugin package must be a .py or .whl file, got: " + extension;
return false;
}
if (extension == ".whl") {
if (!read_wheel_plugin_metadata(canonical, descriptor, error))
return false;
} else {
if (!read_python_plugin_metadata(canonical, descriptor, error))
return false;
}
descriptor.entry_path = canonical.string();
descriptor.plugin_key = make_local_plugin_key(canonical.stem().string());
descriptor.metadata_valid = true;
// Leaving plugin_root empty keeps the probe independent of any .install_state.json next to
// the package and avoids the production sidecar path.
descriptor.plugin_root.clear();
if (descriptor.name.empty())
descriptor.name = descriptor.plugin_key;
return true;
}
void redirect_python_stdout()
{
py::gil_scoped_acquire gil;
py::object sys = py::module_::import("sys");
// Keep plugin prints from corrupting the C++ report on stdout. sys.stderr is the interpreter's
// tee to the temporary log, so plugin output remains available there.
sys.attr("stdout") = sys.attr("stderr");
}
template<class Function>
void invoke_callback(const std::shared_ptr<PluginCapabilityInterface>& capability,
const std::string& phase,
const std::string& callback,
PluginProbeCollector& collector,
Function&& function)
{
ScopedPluginAuditPhase phase_scope(phase);
try {
std::forward<Function>(function)();
collector.record_callback({capability ? capability->name() : std::string{}, phase, callback, "invoked", {}});
} catch (const std::exception& error) {
collector.record_callback({capability ? capability->name() : std::string{}, phase, callback, "failed", error.what()});
} catch (...) {
collector.record_callback({capability ? capability->name() : std::string{}, phase, callback, "failed", "non-standard exception"});
}
}
struct ProbeCapability
{
std::string name;
std::string type;
bool enabled = true;
};
std::vector<ProbeCapability> describe_capabilities(const Plugin& plugin)
{
std::vector<ProbeCapability> capabilities;
capabilities.reserve(plugin.capabilities.size());
for (const auto& capability : plugin.capabilities) {
if (!capability)
continue;
capabilities.push_back({capability->name(), plugin_capability_type_to_string(capability->type()), capability->is_enabled()});
}
return capabilities;
}
void exercise_callbacks(const Plugin& plugin, PluginProbeCollector& collector)
{
// on_load() already ran inside plugin_loader::load. The callbacks below are the additional
// entry points a loaded capability exposes to the host. Synthetic inputs are used; a callback
// may behave differently with a real application state, which the report cannot capture.
static constexpr LifecycleEvent lifecycle_events[] = {
LifecycleEvent::NewProject, LifecycleEvent::ProjectOpened, LifecycleEvent::ProjectBeforeSave,
LifecycleEvent::ProjectAfterSave, LifecycleEvent::ProjectClosed, LifecycleEvent::ProjectDirtyChanged,
LifecycleEvent::SliceStarted, LifecycleEvent::SliceGeometryFinished, LifecycleEvent::GCodeExportStarted,
LifecycleEvent::GCodeExportFinished, LifecycleEvent::SlicingJobComplete, LifecycleEvent::ObjectAdded,
LifecycleEvent::ObjectDeleted, LifecycleEvent::ObjectTransformed, LifecycleEvent::ObjectChanged,
LifecycleEvent::ObjectRenamed, LifecycleEvent::PlateCreated, LifecycleEvent::PlateDeleted,
LifecycleEvent::PlateSelected, LifecycleEvent::PlateRenamed, LifecycleEvent::PresetSelected,
LifecycleEvent::PresetSaved, LifecycleEvent::PrintStateChanged, LifecycleEvent::DeviceOnline,
LifecycleEvent::DeviceOffline, LifecycleEvent::DeviceDiscovered, LifecycleEvent::DeviceSelected,
LifecycleEvent::UploadStarted, LifecycleEvent::UploadFinished, LifecycleEvent::PrintJobStarted,
LifecycleEvent::PrintJobFinished, LifecycleEvent::SendJobStarted, LifecycleEvent::SendJobFinished,
};
for (const auto& capability : plugin.capabilities) {
if (!capability)
continue;
invoke_callback(capability, "callback:get_default_config", "get_default_config", collector,
[&capability]() { (void) capability->get_default_config(); });
if (capability->config_ui_available()) {
invoke_callback(capability, "callback:get_config_ui", "get_config_ui", collector,
[&capability]() { (void) capability->get_config_ui(); });
} else {
collector.record_callback(
{capability->name(), "callback:get_config_ui", "get_config_ui", "skipped", "has_config_ui() is false"});
}
for (const LifecycleEvent event : lifecycle_events) {
const std::string event_name = lifecycle_event_to_string(event);
const std::string phase = "callback:on_lifecycle_event(" + event_name + ")";
LifecycleEventContext context;
context.source = "orca-plugin-probe";
context.msg = "synthetic event generated by the permission probe";
invoke_callback(capability, phase, "on_lifecycle_event(" + event_name + ")", collector,
[&capability, event, &context]() { capability->on_lifecycle_event(event, context); });
}
// Cancellation is a load-time path; calling it after a successful load would misrepresent
// the capability's real lifecycle. Report it as skipped instead.
collector.record_callback(
{capability->name(), "callback:on_cancelled", "on_cancelled", "skipped", "not invoked after a successful load"});
}
}
nlohmann::json observation_json(const AuditObservation& observation)
{
return {
{"plugin", observation.plugin_key},
{"capability", observation.capability},
{"phase", observation.phase.empty() ? "unattributed" : observation.phase},
{"event", observation.event_name},
{"category", observation.category},
{"targets", observation.targets},
{"continued", observation.continued},
};
}
nlohmann::json callback_json(const ProbeCallbackRecord& callback)
{
return {
{"capability", callback.capability}, {"phase", callback.phase}, {"callback", callback.callback},
{"status", callback.status}, {"detail", callback.detail},
};
}
nlohmann::json capability_json(const ProbeCapability& capability)
{
return {
{"name", capability.name},
{"type", capability.type},
{"enabled", capability.enabled},
};
}
nlohmann::json build_report(const Options& options,
const PluginProbeCollector& collector,
const std::vector<ProbeCapability>& capabilities,
const std::string& plugin_key,
bool python_initialized,
const std::string& python_error,
bool loaded,
const std::string& load_error,
const std::string& data_dir)
{
nlohmann::json observations = nlohmann::json::array();
for (const AuditObservation& observation : collector.observations())
observations.push_back(observation_json(observation));
nlohmann::json callbacks = nlohmann::json::array();
for (const ProbeCallbackRecord& callback : collector.callbacks())
callbacks.push_back(callback_json(callback));
nlohmann::json capability_list = nlohmann::json::array();
for (const ProbeCapability& capability : capabilities)
capability_list.push_back(capability_json(capability));
return {
{"tool", "orca-plugin-permission-probe"},
{"mode", "record-only"},
{"observed_by_this_run_only", true},
{"plugin",
{
{"path", options.plugin_path.string()},
{"key", plugin_key},
}},
{"environment",
{
{"python_initialized", python_initialized},
{"python_error", python_error},
{"temporary_data_dir", data_dir},
{"temporary_data_dir_removed_on_exit", true},
{"working_directory", data_dir},
{"interactive_prompts", false},
{"grants_persisted", false},
}},
{"load", {{"ok", loaded}, {"error", load_error}, {"capabilities", capability_list}}},
{"callbacks", callbacks},
{"observations", observations},
{"summary", collector.summary()},
{"coverage",
{
{"exhaustive", false},
{"sandbox", false},
{"plugin_code_executed", true},
{"notes",
{
"Only the code paths this probe exercised are represented. Audit events cover "
"particular operations, not every Python call.",
"The probe does not start the GUI. Host APIs that require a live application or "
"UI (orca.host.app/presets/ui) and plugin.storage report an explicit unsupported "
"error and are reported as failed callbacks.",
"The probe links libslic3r for core geometry/print types, but not libslic3r_gui "
"or wx.",
"The temporary data directory isolates OrcaSlicer state, not plugin-authored file, "
"network or process access. Use OS-level restrictions for untrusted packages.",
"The loader may extract adjacent .whl dependencies into the package's directory; "
"probe a copy of the package when that side effect is unwanted.",
"Import, registration and on_load share one loader phase; the production loader "
"does not expose a finer boundary without changing its behavior.",
"Callbacks run with synthetic inputs, which can change plugin control flow "
"compared with a real application session.",
"continued=true means the audit hook did not block the operation. A surrounding "
"callback or load step can still fail afterwards, as reported in callbacks/load.",
"Events raised on a plugin-spawned thread carry no phase label and are reported "
"as unattributed.",
"Plugin code can spawn threads/processes, write anywhere this process can, or "
"terminate the process. None of that is contained by the probe.",
}},
}},
};
}
void print_text_report(const nlohmann::json& report)
{
std::cout << "OrcaSlicer plugin permission probe (record-only)\n"
"WARNING: observational only; not exhaustive and not a sandbox.\n\n";
std::cout << "Plugin: " << report["plugin"]["path"].get<std::string>() << "\n";
std::cout << "Python initialized: " << (report["environment"]["python_initialized"].get<bool>() ? "yes" : "no") << "\n";
if (!report["environment"]["python_error"].get<std::string>().empty())
std::cout << "Python error: " << report["environment"]["python_error"].get<std::string>() << "\n";
std::cout << "Load: " << (report["load"]["ok"].get<bool>() ? "ok" : "failed") << "\n";
if (!report["load"]["error"].get<std::string>().empty())
std::cout << "Load error: " << report["load"]["error"].get<std::string>() << "\n";
std::cout << "\nCapabilities (" << report["load"]["capabilities"].size() << "):\n";
for (const auto& capability : report["load"]["capabilities"])
std::cout << " " << capability["name"].get<std::string>() << " [" << capability["type"].get<std::string>()
<< "] enabled=" << (capability["enabled"].get<bool>() ? "yes" : "no") << "\n";
std::cout << "\nCallbacks (" << report["callbacks"].size() << "):\n";
for (const auto& callback : report["callbacks"]) {
std::cout << " " << callback["status"].get<std::string>() << " " << callback["callback"].get<std::string>();
if (!callback["capability"].get<std::string>().empty())
std::cout << " capability=" << callback["capability"].get<std::string>();
if (!callback["detail"].get<std::string>().empty())
std::cout << " - " << callback["detail"].get<std::string>();
std::cout << "\n";
}
std::cout << "\nObserved audit events (" << report["observations"].size() << "):\n";
for (const auto& observation : report["observations"]) {
std::cout << " phase=" << observation["phase"].get<std::string>() << " event=" << observation["event"].get<std::string>()
<< " category=" << observation["category"].get<std::string>();
if (!observation["capability"].get<std::string>().empty())
std::cout << " capability=" << observation["capability"].get<std::string>();
if (!observation["targets"].empty()) {
std::cout << " targets=";
bool first = true;
for (const auto& target : observation["targets"]) {
if (!first)
std::cout << ",";
std::cout << target.get<std::string>();
first = false;
}
}
std::cout << " continued=" << (observation["continued"].get<bool>() ? "yes" : "no") << "\n";
}
std::cout << "\nObserved permission categories:\n";
for (const auto& [category, detail] : report["summary"]["categories"].items()) {
std::cout << " " << category << ":";
for (const auto& event : detail["events"])
std::cout << " " << event.get<std::string>();
if (!detail["targets"].empty()) {
std::cout << " -> ";
bool first = true;
for (const auto& target : detail["targets"]) {
if (!first)
std::cout << ", ";
std::cout << target.get<std::string>();
first = false;
}
}
std::cout << "\n";
}
if (report["summary"]["categories"].empty())
std::cout << " (none observed)\n";
std::cout << "\nCoverage limits:\n";
for (const auto& note : report["coverage"]["notes"])
std::cout << " - " << note.get<std::string>() << "\n";
}
} // namespace
int main(int argc, char** argv)
{
Options options;
std::string option_error;
if (!parse_options(argc, argv, options, option_error)) {
std::cerr << "orca-plugin-probe: " << option_error << "\n\n";
print_usage(std::cerr);
return 2;
}
boost::system::error_code ec;
const fs::path canonical_plugin = fs::weakly_canonical(options.plugin_path, ec);
if (!ec)
options.plugin_path = canonical_plugin;
ScopedProbeWorkspace workspace;
PluginProbeCollector collector;
ScopedObservationObserver observer(collector);
PluginDescriptor descriptor;
std::string descriptor_error;
const bool descriptor_ready = prepare_descriptor(options.plugin_path, descriptor, descriptor_error);
PythonInterpreter& interpreter = PythonInterpreter::instance();
bool python_initialized = false;
std::string python_error;
if (descriptor_ready) {
python_initialized = interpreter.initialize();
if (!python_initialized)
python_error = interpreter.last_error();
}
if (python_initialized) {
try {
redirect_python_stdout();
} catch (const std::exception& error) {
python_error = std::string("Could not redirect Python stdout: ") + error.what();
}
}
bool loaded = false;
std::string load_error = descriptor_error;
Plugin plugin;
std::vector<ProbeCapability> capabilities;
if (python_initialized && descriptor_ready) {
// The outer context covers import, registration and on_load in plugin_loader::load; the
// per-callback scopes below cover the follow-up calls. The production loader is unchanged.
ScopedPluginAuditContext audit_context(descriptor.plugin_key);
{
ScopedPluginAuditPhase load_phase(kLoadPhase);
loaded = plugin_loader::load(descriptor, /*skip_deps=*/true, /*capabilities_to_enable=*/{}, nullptr, plugin, load_error);
collector.record_callback({std::string{}, kLoadPhase, "plugin_loader::load", loaded ? "invoked" : "failed", load_error});
collector.record_callback({std::string{}, "callback:on_load", "on_load", loaded ? "invoked" : "skipped",
loaded ? "invoked by plugin_loader::load for each materialized capability" :
"plugin_loader::load failed before completing on_load"});
}
capabilities = describe_capabilities(plugin);
if (loaded)
exercise_callbacks(plugin, collector);
if (loaded) {
ScopedPluginAuditPhase unload_phase("callback:on_unload");
plugin_loader::unload(plugin);
collector.record_callback({std::string{}, "callback:on_unload", "on_unload", "invoked",
"invoked by plugin_loader::unload; per-capability failures are logged by the loader"});
}
}
if (interpreter.is_initialized())
interpreter.shutdown();
const nlohmann::json report = build_report(options, collector, capabilities,
descriptor.plugin_key.empty() ? make_local_plugin_key(options.plugin_path.stem().string()) :
descriptor.plugin_key,
python_initialized, python_error, loaded, load_error, workspace.root().string());
if (options.json)
std::cout << report.dump(2, ' ', false, nlohmann::json::error_handler_t::replace) << "\n";
else
print_text_report(report);
return loaded ? 0 : 1;
}
@@ -1,97 +0,0 @@
#ifndef slic3r_PluginProbeCollector_hpp_
#define slic3r_PluginProbeCollector_hpp_
// Record-only collection for the headless plugin permission probe. This is deliberately a plain
// in-memory store: it has no dialog, grant or persistence path, and it never decides whether an
// audited operation may proceed. Keeping it independent of wx/Python also makes its grouping and
// phase attribution testable without starting the interpreter.
#include "PluginAuditManager.hpp"
#include <nlohmann/json.hpp>
#include <map>
#include <mutex>
#include <set>
#include <string>
#include <utility>
#include <vector>
namespace Slic3r {
// What the probe attempted and what came back. `status` is one of "invoked", "failed" or
// "skipped"; a skipped callback is reported so the absence of observations is not mistaken for a
// clean result.
struct ProbeCallbackRecord
{
std::string capability;
std::string phase;
std::string callback;
std::string status;
std::string detail;
};
class PluginProbeCollector
{
public:
void record(const AuditObservation& observation)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_observations.push_back(observation);
}
void record_callback(ProbeCallbackRecord record)
{
std::lock_guard<std::mutex> lock(m_mutex);
m_callbacks.push_back(std::move(record));
}
std::vector<AuditObservation> observations() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_observations;
}
std::vector<ProbeCallbackRecord> callbacks() const
{
std::lock_guard<std::mutex> lock(m_mutex);
return m_callbacks;
}
// Groups the run by the permission categories the audit hook already assigned, and lists the
// distinct targets and event names seen in each. This is a view of this run only; it says
// nothing about code paths the plugin did not exercise.
nlohmann::json summary() const
{
std::lock_guard<std::mutex> lock(m_mutex);
std::map<std::string, std::set<std::string>> events;
std::map<std::string, std::set<std::string>> targets;
for (const AuditObservation& observation : m_observations) {
events[observation.category].insert(observation.event_name);
targets[observation.category].insert(observation.targets.begin(), observation.targets.end());
}
nlohmann::json categories = nlohmann::json::object();
for (const auto& [category, category_events] : events) {
categories[category] = {
{"events", category_events},
{"targets", targets[category]},
};
}
return {
{"observed_event_count", m_observations.size()},
{"categories", categories},
};
}
private:
mutable std::mutex m_mutex;
std::vector<AuditObservation> m_observations;
std::vector<ProbeCallbackRecord> m_callbacks;
};
} // namespace Slic3r
#endif // slic3r_PluginProbeCollector_hpp_
-44
View File
@@ -1,44 +0,0 @@
#pragma once
// User-facing plugin messages are translated in the GUI build. The headless probe links neither
// libslic3r_gui nor wx, so it formats the same English text without wxGetTranslation. The GUI path
// expands to exactly the calls the plugin sources used before this header existed.
#if defined(ORCA_PLUGIN_PROBE_BUILD)
#include "libslic3r/format.hpp"
#include <string>
#include <utility>
namespace Slic3r::plugin_text {
inline std::string translate_utf8(const char* text) { return text == nullptr ? std::string{} : std::string(text); }
inline std::string translate_utf8(const std::string& text) { return text; }
template<typename... TArgs>
inline std::string format(const char* fmt, TArgs&&... args)
{
return Slic3r::format(fmt, std::forward<TArgs>(args)...);
}
template<typename... TArgs>
inline std::string format(const std::string& fmt, TArgs&&... args)
{
return Slic3r::format(fmt, std::forward<TArgs>(args)...);
}
} // namespace Slic3r::plugin_text
#define PLUGIN_U8L(text) ::Slic3r::plugin_text::translate_utf8(text)
#define PLUGIN_FORMAT(...) ::Slic3r::plugin_text::format(__VA_ARGS__)
#else
#include "slic3r/GUI/I18N.hpp"
#include "slic3r/GUI/format.hpp"
#define PLUGIN_U8L(text) _u8L(text)
#define PLUGIN_FORMAT(...) GUI::format(__VA_ARGS__)
#endif
+2 -2
View File
@@ -30,7 +30,7 @@
#include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp" #include "pluginTypes/slicingPipeline/SlicingPipelinePluginCapability.hpp"
#include <pybind11/pytypes.h> #include <pybind11/pytypes.h>
#include <pybind11/gil.h> #include <pybind11/gil.h>
#include "PluginText.hpp" #include "slic3r/GUI/I18N.hpp"
namespace py = pybind11; namespace py = pybind11;
@@ -193,7 +193,7 @@ std::vector<CapturedCapability> PythonPluginBridge::finalize_plugin_capture(cons
granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions); granted = PluginAuditManager::instance().request_filesystem_read_permissions(plugin_key, fs_read_permissions);
} }
if (!granted) { if (!granted) {
error = PLUGIN_U8L("Plugin filesystem read permission request denied"); error = _u8L("Plugin filesystem read permission request denied");
BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key; BOOST_LOG_TRIVIAL(warning) << error << " for key " << plugin_key;
discard_pending_for_key(); discard_pending_for_key();
clear_active_key(); clear_active_key();
@@ -1,4 +0,0 @@
// libslic3r references NanoSVG without carrying its header-only implementation. This probe links
// libslic3r without the GUI BitmapCache.cpp that supplies the implementation in the application.
#define NANOSVG_IMPLEMENTATION
#include "nanosvg/nanosvg.h"
@@ -1,32 +0,0 @@
// Headless replacement for the GUI permission front end. The probe installs an observation
// observer, so the audit hook records and returns before reaching this seam. If that observer is
// ever absent, permission decisions fail closed instead of consulting GUI or install state.
#include "slic3r/plugin/PluginAuditManagerInternal.hpp"
#include <boost/log/trivial.hpp>
#include <string>
#include <vector>
namespace Slic3r::plugin_audit {
bool request_filesystem_read_permissions(const std::string& plugin_key,
const std::vector<std::string>& paths)
{
BOOST_LOG_TRIVIAL(warning) << "[PROBE] Refusing filesystem read permission request for " << plugin_key
<< " (" << paths.size() << " path(s)): the headless probe does not evaluate grants";
return false;
}
int decide_event(PluginAuditManager& mgr,
const std::string& event_name,
AuditEventCategory category,
const std::vector<std::string>& targets)
{
(void) category;
(void) targets;
return report_denied(mgr, event_name, {false, "permission decisions are unavailable in the headless probe"});
}
} // namespace Slic3r::plugin_audit
@@ -1,75 +0,0 @@
// Probe-only `orca.host` registration. The GUI-free domains use the production bindings; the
// domains that need a live GUI application, main window or preset bundle are exposed as explicit
// unsupported submodules instead of being compiled (and linked) into the probe.
//
// A plugin that touches one of those domains fails its own callback with a clear message; the
// failure is recorded by the probe's callback report rather than becoming a link dependency.
#include "slic3r/plugin/host/PluginHost.hpp"
#include "slic3r/plugin/host/PluginHostBindings.hpp"
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <stdexcept>
#include <string>
namespace py = pybind11;
namespace Slic3r {
namespace {
void add_unavailable_domain(py::module_& host, const char* name, const char* description)
{
py::module_ domain = host.def_submodule(name, description);
const std::string prefix = std::string("orca.host.") + name + ".";
domain.attr("__getattr__") = py::cpp_function([prefix](const std::string& attribute) -> py::object {
throw std::runtime_error(prefix + attribute +
" is not available in the headless plugin permission probe because it needs the GUI application");
});
}
void add_unavailable_function(py::module_& host, const char* name)
{
// Production registers these in PluginHostApp::register_app(). Do not overwrite a slot that
// another registrar already claimed (the model domain binds the Model class through
// register_model); the probe only fills the GUI-backed accessors that are absent here.
if (py::hasattr(host, name))
return;
const std::string detail = std::string("orca.host.") + name +
" is not available in the headless plugin permission probe because it needs the GUI application";
host.def(name, [detail](py::args, py::kwargs) -> py::object { throw std::runtime_error(detail); }, detail.c_str());
}
} // namespace
void PluginHost::RegisterBindings(py::module_& module)
{
auto host = module.def_submodule("host", "Host application API");
// Value types first so the docstring signatures of later registrars resolve to the bound names.
host_bindings::register_geometry(host);
host_bindings::register_mesh(host);
host_bindings::register_model(host);
host_bindings::register_slicing(host);
// These domains are backed by the live GUI, its Plater/MainFrame and the preset bundle.
add_unavailable_domain(host, "presets", "Preset bundle access (headless probe: unavailable)");
add_unavailable_domain(host, "app", "Live application and Plater access (headless probe: unavailable)");
add_unavailable_domain(host, "ui", "Native dialogs and HTML windows (headless probe: unavailable)");
for (const char* name : {"plater", "model", "preset_bundle", "app_info", "gl_info", "selected_printer"})
add_unavailable_function(host, name);
// Storage needs the PluginManager registry, which is not part of the headless probe.
auto plugin_host = host.def_submodule("plugin", "Plugin host API (headless probe: storage unavailable)");
plugin_host.def("storage", []() -> std::string {
throw std::runtime_error("orca.host.plugin.storage() is not available in the headless plugin permission probe");
}, "Return the installed folder of the current plugin.");
plugin_host.attr("__getattr__") = py::cpp_function([](const std::string& attribute) -> py::object {
throw std::runtime_error("orca.host.plugin." + attribute +
" is not available in the headless plugin permission probe");
});
}
} // namespace Slic3r
@@ -1,32 +0,0 @@
// Minimal replacement for the config API PythonPluginBridge binds into `orca`.
//
// The production implementation (PluginConfig.cpp) resolves presets, reads the sidebar's effective
// config and writes config.json through PluginManager, all of which pull in the GUI and the
// registry. The probe has neither: a capability reports an empty config, an empty version and a
// refused save. That keeps the bridge's Python surface intact without pretending to reproduce
// application state the probe does not have.
#include "slic3r/plugin/PluginConfig.hpp"
#include "slic3r/plugin/PythonPluginInterface.hpp"
#include <nlohmann/json.hpp>
#include <string>
namespace Slic3r {
nlohmann::json capability_get_config(const PluginCapabilityInterface&)
{
return nlohmann::json::object();
}
std::string capability_get_config_version(const PluginCapabilityInterface&) { return {}; }
bool capability_save_config(const PluginCapabilityInterface&, const nlohmann::json&) { return false; }
std::string PluginConfig::capability_type_label(PluginCapabilityType type)
{
return plugin_capability_type_display_name(type);
}
} // namespace Slic3r
+1
View File
@@ -42,6 +42,7 @@ add_executable(${_TEST_NAME}_tests
test_fill_plane_path.cpp test_fill_plane_path.cpp
test_adaptive_infill_field.cpp test_adaptive_infill_field.cpp
test_geometry.cpp test_geometry.cpp
test_kdtree.cpp
test_imex_arrange.cpp test_imex_arrange.cpp
test_imex_helpers.cpp test_imex_helpers.cpp
test_imex_zones.cpp test_imex_zones.cpp
+90
View File
@@ -7,11 +7,14 @@
#include "libslic3r/libslic3r.h" #include "libslic3r/libslic3r.h"
#include <numeric> #include <numeric>
#include <iostream> #include <iostream>
#include <utility>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
#include <utility> #include <utility>
#include <vector> #include <vector>
#include <cstdlib>
#include <catch2/catch_test_macros.hpp> #include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_approx.hpp> #include <catch2/catch_approx.hpp>
#include "libslic3r/ClipperUtils.hpp" #include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp" #include "libslic3r/ExPolygon.hpp"
@@ -300,3 +303,90 @@ TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") {
REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested))); REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested)));
REQUIRE(top_level_expolygons(reference).size() == 1); 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));
}
+72
View File
@@ -0,0 +1,72 @@
#include <catch2/catch_all.hpp>
#include <numeric>
#include <random>
#include <vector>
#include <algorithm>
#include <cstddef>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#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);
}
-88
View File
@@ -3,13 +3,11 @@
#include <boost/system/error_code.hpp> #include <boost/system/error_code.hpp>
#include <catch2/catch_all.hpp> #include <catch2/catch_all.hpp>
#include <cstddef>
#include <libslic3r/Utils.hpp> #include <libslic3r/Utils.hpp>
#include <libslic3r/libslic3r.h> // GCODEVIEWER_APP_KEY, SLIC3R_APP_KEY (via libslic3r_version.h) #include <libslic3r/libslic3r.h> // GCODEVIEWER_APP_KEY, SLIC3R_APP_KEY (via libslic3r_version.h)
#include <slic3r/plugin/PluginAuditManager.hpp> #include <slic3r/plugin/PluginAuditManager.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp> #include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp> #include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PluginProbeCollector.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp> #include <slic3r/plugin/PythonInterpreter.hpp>
#include <slic3r/plugin/PythonPluginBridge.hpp> #include <slic3r/plugin/PythonPluginBridge.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp> #include <slic3r/plugin/PythonPluginInterface.hpp>
@@ -34,7 +32,6 @@
#include <pybind11/pytypes.h> #include <pybind11/pytypes.h>
#include <pybind11/stl.h> #include <pybind11/stl.h>
#include <algorithm>
#include <fstream> #include <fstream>
#include <functional> #include <functional>
#include <ios> #include <ios>
@@ -70,26 +67,6 @@ void seed_denied_keywords()
mgr.add_denied_path_keyword(keyword); mgr.add_denied_path_keyword(keyword);
} }
// Installs the record-only observer for a test and always clears it, so a failed CHECK cannot
// leave a callback pointing at a destroyed collector for the next test in this process.
struct ScopedTestObservationObserver
{
explicit ScopedTestObservationObserver(PluginProbeCollector& collector)
: m_collector(collector)
{
PluginAuditManager::instance().set_observation_observer(
[this](const AuditObservation& observation) { m_collector.record(observation); });
}
~ScopedTestObservationObserver() { PluginAuditManager::instance().clear_observation_observer(); }
ScopedTestObservationObserver(const ScopedTestObservationObserver&) = delete;
ScopedTestObservationObserver& operator=(const ScopedTestObservationObserver&) = delete;
private:
PluginProbeCollector& m_collector;
};
} // namespace } // namespace
TEST_CASE("Plugin audit denies host-managed filenames anywhere", "[audit]") TEST_CASE("Plugin audit denies host-managed filenames anywhere", "[audit]")
@@ -690,71 +667,6 @@ const char* const PLUGIN = "AuditPlugin";
} // namespace } // namespace
TEST_CASE("Probe phase scope restores the previous phase", "[audit]")
{
PluginAuditManager& mgr = PluginAuditManager::instance();
mgr.set_current_audit_phase("outer");
{
ScopedPluginAuditPhase inner("inner");
CHECK(mgr.current_audit_phase() == "inner");
}
CHECK(mgr.current_audit_phase() == "outer");
mgr.clear_current_audit_phase();
CHECK(mgr.current_audit_phase().empty());
}
TEST_CASE("Record-only probe observations bypass prompts and retain phase attribution", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
py::gil_scoped_acquire gil;
PythonSide python(harness);
PluginProbeCollector collector;
ScopedTestObservationObserver observer(collector);
// A declared read permission is recorded and allowed, but must not be written to the sidecar.
{
ScopedPluginAuditContext context(PLUGIN);
CHECK(PluginAuditManager::instance().request_filesystem_read_permissions(PLUGIN, {"/tmp/probe-declared"}));
}
{
ScopedPluginAuditPhase phase("probe:callback");
python.run_as(PLUGIN, "outcome = attempt(lambda: open(PRIVATE).close())");
}
CHECK(python.str("outcome") == "ok");
CHECK(harness.prompts.empty());
CHECK(harness.saved_permissions(PLUGIN).fs_read.empty());
const std::vector<AuditObservation> observations = collector.observations();
const auto open_event = std::find_if(
observations.begin(), observations.end(), [](const AuditObservation& observation) {
return observation.event_name == "open";
});
REQUIRE(open_event != observations.end());
CHECK(open_event->plugin_key == PLUGIN);
CHECK(open_event->phase == "probe:callback");
CHECK(open_event->category == "FsRead");
CHECK(open_event->continued);
CHECK(std::find(open_event->targets.begin(), open_event->targets.end(), python.str("PRIVATE")) !=
open_event->targets.end());
const auto declared = std::find_if(observations.begin(), observations.end(), [](const AuditObservation& observation) {
return observation.event_name == "orca.request_permissions";
});
REQUIRE(declared != observations.end());
CHECK(declared->category == "FsRead");
CHECK(std::find(declared->targets.begin(), declared->targets.end(), "/tmp/probe-declared") != declared->targets.end());
const nlohmann::json summary = collector.summary();
CHECK(summary["categories"].contains("FsRead"));
CHECK(summary["observed_event_count"].get<std::size_t>() == observations.size());
}
TEST_CASE("A saved request grant covers the request's own connection in a later session", "[audit][Python]") TEST_CASE("A saved request grant covers the request's own connection in a later session", "[audit][Python]")
{ {
AuditHarness harness; AuditHarness harness;