Compare commits

..
48 changed files with 2107 additions and 1986 deletions

No files matched your search

@@ -1,8 +1,8 @@
# Adaptive infill — High Level Design
# Adaptive TPMS infill — High Level Design
## Purpose and scope
`adaptive_infill_mode` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
`tpms_adaptive` grades the sparse infill of the Gyroid, TPMS-D and TPMS-FK
patterns inside the object: the cells grow continuously from the surface
towards the center of the object. `distance_warp`, `smooth_blend` and
`stepped_shells` follow the distance to the nearest surface, including the top
@@ -10,8 +10,8 @@ and bottom, like concentric shells; `lobes` follows the whole 3D shape towards
the center of each lobe of the object; `normal_z`, `normal_y` and `normal_x`
follow each section of the object normal to that axis, so the grading does not
change along the axis, as suits a profile extruded along it.
`sparse_infill_density` is the density at the surface, `adaptive_infill_interior_density`
the density at the center, and `adaptive_infill_gradient` picks how the density
`sparse_infill_density` is the density at the surface, `tpms_interior_density`
the density at the center, and `tpms_adaptive_gradient` picks how the density
goes from one to the other. Only internal sparse infill is graded; the Gyroid
Z-buckling optimization does not apply to it.
@@ -22,9 +22,9 @@ split into shells or blended between densities.
## Radial field
`AdaptiveInfillField` gives every point of an object the center of its lobe and a
`TpmsRadialField` gives every point of an object the center of its lobe and a
radial coordinate: 0 at the center, 1 at the surface along the ray from the
center. `PrintObject::prepare_adaptive_infill_fields()` builds it in
center. `PrintObject::prepare_tpms_radial_fields()` builds it in
`bridge_over_infill()`, next to the adaptive cubic octree, because the anchoring
infill generated there has to match the printed infill. A field is built for
every mode a region uses, and is shared by the regions using that mode: the
@@ -202,7 +202,7 @@ built for them.
## Constraints
- With `adaptive_infill_mode` disabled, or for other patterns, the fill parameters are reset
- With `tpms_adaptive` disabled, or for other patterns, the fill parameters are reset
to their defaults, so they neither change the infill nor split fill batches.
- `Layer::get_sparse_infill_max_void_area()` uses the sparser of the two
densities, as the voids at the center are that large.
-17
View File
@@ -62,23 +62,6 @@ Clipper2 behaves differently by default, the wrapper adjusts it.
clip do not leave slivers.
- Open polylines are clipped with the non-zero rule and keep their direction.
### Tiled booleans
The sweep slows down with the number of edges crossing a scan line, so a layer
cut into thousands of pieces makes every whole-layer boolean expensive.
`diff_ex_by_piece()` and `intersection_ex_by_piece()` take a subject of
non-overlapping `ExPolygons`, group them into tiles with
`ClipperUtils::tile_expolygons()`, and run each tile in parallel against only
the clip polygons near it, cut to the tile's box. Below 128 pieces there is a
single tile, and they are the plain `diff_ex()` / `intersection_ex()`.
The result covers the same area as the plain call. Without the safety offset
the rings are the same. With it, each tile unites only the clip polygons near
it, so a clip edge that the whole-layer union splits where it crosses a distant
clip polygon stays whole, and a crossing with the subject can round 1 unit
differently. The tiles' results are concatenated in tile order, so the order of
the output `ExPolygons` differs from the plain call.
### Offsets
- Before offsetting, input vertices closer than
+2 -2
View File
@@ -182,8 +182,8 @@ set(lisbslic3r_sources
Fill/FillPlanePath.hpp
Fill/FillRectilinear.cpp
Fill/FillRectilinear.hpp
Fill/AdaptiveInfillField.cpp
Fill/AdaptiveInfillField.hpp
Fill/FillTpmsAdaptive.cpp
Fill/FillTpmsAdaptive.hpp
Fill/FillTpmsD.cpp
Fill/FillTpmsD.hpp
Fill/FillTpmsFK.cpp
-68
View File
@@ -9,8 +9,6 @@
#include <numeric>
#include <unordered_map>
#include <tbb/parallel_for.h>
#include "ClipperUtils.hpp"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
@@ -802,72 +800,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesProvider(subject), ClipperUtils::SurfacesProvider(clip), do_safety_offset); }
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset)
{ return _clipper_ex(ctIntersection, ClipperUtils::SurfacesPtrProvider(subject), ClipperUtils::ExPolygonsProvider(clip), do_safety_offset); }
namespace ClipperUtils {
std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile)
{
BoundingBox extent;
std::vector<BoundingBox> bboxes;
bboxes.reserve(expolygons.size());
for (const ExPolygon &expoly : expolygons) {
bboxes.emplace_back(get_extents(expoly));
extent.merge(bboxes.back());
}
if (! extent.defined)
return {};
const int tiles = std::clamp(int(std::sqrt(double(expolygons.size()) / double(std::max<size_t>(per_tile, 1)))), 1, 32);
const Point size = extent.size();
const coord_t tile_w = std::max<coord_t>(1, size.x() / tiles + 1), tile_h = std::max<coord_t>(1, size.y() / tiles + 1);
std::vector<ExPolygonsTile> out(size_t(tiles * tiles));
for (size_t i = 0; i < expolygons.size(); ++ i) {
const Point c = bboxes[i].center();
ExPolygonsTile &tile = out[size_t(std::clamp(int((c.y() - extent.min.y()) / tile_h), 0, tiles - 1) * tiles +
std::clamp(int((c.x() - extent.min.x()) / tile_w), 0, tiles - 1))];
tile.members.emplace_back(i);
tile.bbox.merge(bboxes[i]);
}
out.erase(std::remove_if(out.begin(), out.end(), [](const ExPolygonsTile &tile) { return tile.members.empty(); }), out.end());
return out;
}
}
static Slic3r::ExPolygons clipper_ex_by_piece(ClipType clipType, const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{
// A few dozen subject ExPolygons to a tile, each tile one Clipper call with the clip cut to the tile's box.
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(subject, 32);
// One tile is the plain call: cutting the clip would only cost time.
if (tiles.size() <= 1)
return _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(subject), ClipperUtils::PolygonsProvider(clip), do_safety_offset);
std::vector<BoundingBox> clip_bboxes;
clip_bboxes.reserve(clip.size());
for (const Polygon &polygon : clip)
clip_bboxes.emplace_back(get_extents(polygon));
std::vector<Slic3r::ExPolygons> out_tiles(tiles.size());
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
Slic3r::ExPolygons local_subject;
local_subject.reserve(tile.members.size());
for (size_t i : tile.members)
local_subject.emplace_back(subject[i]);
// Grown so that the cut edges of the clip stay clear of the subject, also after the safety offset.
const BoundingBox bbox = tile.bbox.inflated(SCALED_EPSILON);
Polygons local_clip;
for (size_t i = 0; i < clip.size(); ++i)
if (clip_bboxes[i].overlap(bbox))
if (Polygon clipped = ClipperUtils::clip_clipper_polygon_with_subject_bbox(clip[i], bbox); ! clipped.empty())
local_clip.emplace_back(std::move(clipped));
out_tiles[tile_idx] = _clipper_ex(clipType, ClipperUtils::ExPolygonsProvider(local_subject), ClipperUtils::PolygonsProvider(local_clip), do_safety_offset);
});
Slic3r::ExPolygons out;
for (Slic3r::ExPolygons &out_tile : out_tiles)
append(out, std::move(out_tile));
return out;
}
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctDifference, subject, clip, do_safety_offset); }
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset)
{ return clipper_ex_by_piece(ctIntersection, subject, clip, do_safety_offset); }
// May be used to "heal" unusual models (3DLabPrints etc.) by providing fill_type (pftEvenOdd, pftNonZero, pftPositive, pftNegative).
Slic3r::ExPolygons union_ex(const Slic3r::Polygons &subject, PolyFillType fill_type)
{ return _clipper_ex(ctUnion, ClipperUtils::PolygonsProvider(subject), ClipperUtils::EmptyPathsProvider(), ApplySafetyOffset::No, fill_type); }
-15
View File
@@ -5,7 +5,6 @@
#include "Polyline.hpp"
#include "Line.hpp"
#include "libslic3r.h"
#include "BoundingBox.hpp"
#include "ExPolygon.hpp"
#include "Polygon.hpp"
#include "Surface.hpp"
@@ -333,15 +332,6 @@ namespace ClipperUtils {
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygon &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
[[nodiscard]] Polygons clip_clipper_polygons_with_subject_bbox(const ExPolygons &src, const BoundingBox &bbox, const bool get_entire_polygons = false);
// Splits ExPolygons into tiles by the centres of their boxes, about `per_tile` of them to a tile, to run Clipper on a
// layer of many pieces tile by tile. Returns the non-empty tiles, each with the indices of its ExPolygons and their box.
struct ExPolygonsTile
{
BoundingBox bbox;
std::vector<size_t> members;
};
[[nodiscard]] std::vector<ExPolygonsTile> tile_expolygons(const ExPolygons &expolygons, size_t per_tile);
}
// offset Polygons
@@ -537,11 +527,6 @@ Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::Surfaces &subject, const Slic3r::Surfaces &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex(const Slic3r::SurfacesPtr &subject, const Slic3r::ExPolygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
// diff_ex() / intersection_ex() of the subject split into tiles, each against only the part of the clip near it, the tiles in
// parallel. The same area as the operation on the whole subject when its ExPolygons do not overlap, and much faster for a
// subject of thousands of pieces spread over a layer: Clipper slows down with the number of edges crossing a scan line.
Slic3r::ExPolygons diff_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::ExPolygons intersection_ex_by_piece(const Slic3r::ExPolygons &subject, const Slic3r::Polygons &clip, ApplySafetyOffset do_safety_offset = ApplySafetyOffset::No);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::Polygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polyline &subject, const Slic3r::ExPolygon &clip);
Slic3r::Polylines intersection_pl(const Slic3r::Polylines &subject, const Slic3r::ExPolygon &clip);
+35 -54
View File
@@ -22,8 +22,6 @@
#include "../PrintConfig.hpp"
#include "../Surface.hpp"
#include <tbb/parallel_for.h>
#include "AABBTreeLines.hpp"
#include "libslic3r/Config.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -314,9 +312,9 @@ struct SurfaceFillParams
bool gyroid_optimized = false;
// For TPMS: grade the density from the surface to the interior of the object.
AdaptiveInfillMode adaptive_infill_mode = AdaptiveInfillMode::Disabled;
float adaptive_infill_interior_density = 0.f;
AdaptiveInfillGradient adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
TpmsAdaptiveMode tpms_adaptive = TpmsAdaptiveMode::Disabled;
float tpms_interior_density = 0.f;
TpmsAdaptiveGradient tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -360,9 +358,9 @@ struct SurfaceFillParams
RETURN_COMPARE_NON_EQUAL(skin_infill_depth);
RETURN_COMPARE_NON_EQUAL(infill_overhang_angle);
RETURN_COMPARE_NON_EQUAL(gyroid_optimized);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_mode);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_interior_density);
RETURN_COMPARE_NON_EQUAL(adaptive_infill_gradient);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive);
RETURN_COMPARE_NON_EQUAL(tpms_interior_density);
RETURN_COMPARE_NON_EQUAL(tpms_adaptive_gradient);
RETURN_COMPARE_NON_EQUAL(smooth_factor);
RETURN_COMPARE_NON_EQUAL(center_of_surface_pattern);
RETURN_COMPARE_NON_EQUAL(separated_infills);
@@ -396,9 +394,9 @@ struct SurfaceFillParams
this->center_of_surface_pattern == rhs.center_of_surface_pattern &&
this->separated_infills == rhs.separated_infills &&
this->gyroid_optimized == rhs.gyroid_optimized &&
this->adaptive_infill_mode == rhs.adaptive_infill_mode &&
this->adaptive_infill_interior_density == rhs.adaptive_infill_interior_density &&
this->adaptive_infill_gradient == rhs.adaptive_infill_gradient &&
this->tpms_adaptive == rhs.tpms_adaptive &&
this->tpms_interior_density == rhs.tpms_interior_density &&
this->tpms_adaptive_gradient == rhs.tpms_adaptive_gradient &&
this->smooth_factor == rhs.smooth_factor &&
this->fill_order == rhs.fill_order;
}
@@ -677,28 +675,24 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
if (!line_based_pattern) {
const coord_t scaled_spacing = scaled<coord_t>(fill.params.spacing);
// 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];
for (const ExPolygon &expolygon : fill.expolygons) {
Polygons filled_area = to_polygons(expolygon);
// "Core" area: open (erode+dilate) to drop thin features, then clamp back to the original polygon.
Polygons inner_area = intersection(filled_area, opening(filled_area, scaled_spacing, scaled_spacing));
if (inner_area.empty()) {
split_parts[idx].second.emplace_back(expolygon);
return;
narrow_infill.emplace_back(expolygon);
continue;
}
ExPolygons inner_ex = union_ex(inner_area);
ExPolygons expolys{expolygon};
split_parts[idx].second = diff_ex(expolys, inner_ex); // narrow infill area
split_parts[idx].first = intersection_ex(expolys, inner_ex); // normal infill area
});
for (auto &[normal_ex, narrow_ex] : split_parts) {
append(normal_infill, std::move(normal_ex));
append(narrow_infill, std::move(narrow_ex));
ExPolygons narrow_ex = diff_ex(expolys, inner_ex);
ExPolygons normal_ex = intersection_ex(expolys, inner_ex);
append(normal_infill, normal_ex); // normal infill area
append(narrow_infill, narrow_ex); // narrow infill area
}
return;
@@ -720,10 +714,7 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
const double aligning_angle = -base_angle + PI;
// 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];
for (const ExPolygon &expolygon : fill.expolygons) {
Polygons filled_area = to_polygons(expolygon);
polygons_rotate(filled_area, aligning_angle);
BoundingBox bb = get_extents(filled_area);
@@ -854,10 +845,8 @@ void split_solid_surface(size_t layer_id, const SurfaceFill &fill, ExPolygons &n
}
}
split_reconstructed[expolygon_idx] = std::move(reconstructed_area);
});
for (Polygons &reconstructed_area : split_reconstructed)
polygons_append(normal_fill_areas, std::move(reconstructed_area));
polygons_append(normal_fill_areas, reconstructed_area);
}
polygons_rotate(normal_fill_areas, -aligning_angle);
@@ -1021,17 +1010,17 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface;
// Orca: only the TPMS sparse infill is graded; reset otherwise, as params is reused.
params.adaptive_infill_mode = is_adaptive_infill_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.adaptive_infill_mode.value : AdaptiveInfillMode::Disabled;
const bool adaptive_infill_mode = params.adaptive_infill_mode != AdaptiveInfillMode::Disabled;
params.adaptive_infill_interior_density = adaptive_infill_mode ? std::max(1.f, float(region_config.adaptive_infill_interior_density)) : 0.f;
params.adaptive_infill_gradient = adaptive_infill_mode ? region_config.adaptive_infill_gradient.value : AdaptiveInfillGradient::Linear;
params.tpms_adaptive = is_tpms_adaptive_pattern(params.pattern) && params.extrusion_role == erInternalInfill ?
region_config.tpms_adaptive.value : TpmsAdaptiveMode::Disabled;
const bool tpms_adaptive = params.tpms_adaptive != TpmsAdaptiveMode::Disabled;
params.tpms_interior_density = tpms_adaptive ? std::max(1.f, float(region_config.tpms_interior_density)) : 0.f;
params.tpms_adaptive_gradient = tpms_adaptive ? region_config.tpms_adaptive_gradient.value : TpmsAdaptiveGradient::Linear;
// Pass through gyroid_optimized only when the effective pattern is Gyroid,
// so non-Gyroid fills do not differ in SurfaceFillParams by an irrelevant flag
// (which would unnecessarily split fill batching). Adaptive density replaces it.
// Stored on SurfaceFillParams; copied to FillParams during conversion.
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !adaptive_infill_mode;
params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized && !tpms_adaptive;
if (params.extrusion_role == erInternalInfill) {
params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value,
@@ -1384,7 +1373,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
if (surface_fill.params.pattern == ipConcentricInternal) {
@@ -1434,9 +1423,9 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
params.lateral_lattice_angle_2 = surface_fill.params.lateral_lattice_angle_2;
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// BBS
@@ -1484,15 +1473,7 @@ void Layer::make_fills(const FillAdaptive::RegionOctrees* fill_octrees, FillLigh
f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox);
f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr;
// 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);
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
if (params.symmetric_infill_y_axis) {
params.symmetric_y_axis = f->extended_object_bounding_box().center().x();
expoly.symmetric_y(params.symmetric_y_axis);
@@ -1617,7 +1598,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
f->angle = surface_fill.params.angle;
f->fixed_angle = surface_fill.params.fixed_angle;
const FillAdaptive::Octrees *octrees = fill_octrees ? fill_octrees->region(surface_fill.region_id) : nullptr;
f->adaptive_infill_field = this->object()->adaptive_infill_field(surface_fill.params.adaptive_infill_mode);
f->tpms_radial_field = this->object()->tpms_radial_field(surface_fill.params.tpms_adaptive);
f->print_config = &this->object()->print()->config();
f->print_object_config = &this->object()->config();
@@ -1657,9 +1638,9 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdapti
params.infill_overhang_angle = surface_fill.params.infill_overhang_angle;
params.multiline = surface_fill.params.multiline;
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
params.adaptive_infill_mode = surface_fill.params.adaptive_infill_mode;
params.adaptive_infill_interior_density = float(0.01 * surface_fill.params.adaptive_infill_interior_density);
params.adaptive_infill_gradient = surface_fill.params.adaptive_infill_gradient;
params.tpms_adaptive = surface_fill.params.tpms_adaptive;
params.tpms_interior_density = float(0.01 * surface_fill.params.tpms_interior_density);
params.tpms_adaptive_gradient = surface_fill.params.tpms_adaptive_gradient;
params.smooth_factor = surface_fill.params.smooth_factor;
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
// Without the sparse extrusion role, the filler uses each surface's bounds
+5 -5
View File
@@ -33,7 +33,7 @@ namespace Slic3r { class ExtrusionEntityCollection; }
namespace Slic3r {
class Surface;
class AdaptiveInfillField;
class TpmsRadialField;
enum InfillPattern : int;
namespace FillAdaptive {
@@ -93,9 +93,9 @@ struct FillParams
bool gyroid_optimized { false };
// For TPMS: grade the density from the surface to the interior of the object. Density fraction.
AdaptiveInfillMode adaptive_infill_mode { AdaptiveInfillMode::Disabled };
float adaptive_infill_interior_density { 0.f };
AdaptiveInfillGradient adaptive_infill_gradient { AdaptiveInfillGradient::Linear };
TpmsAdaptiveMode tpms_adaptive { TpmsAdaptiveMode::Disabled };
float tpms_interior_density { 0.f };
TpmsAdaptiveGradient tpms_adaptive_gradient { TpmsAdaptiveGradient::Linear };
// Orca: corner smoothing factor in the range [0, 1].
double smooth_factor { 0. };
@@ -162,7 +162,7 @@ public:
FillAdaptive::Octree* adapt_fill_octree = nullptr;
// Radial coordinate inside the object for the adaptive TPMS infill
const AdaptiveInfillField* adaptive_infill_field = nullptr;
const TpmsRadialField* tpms_radial_field = nullptr;
// PrintConfig and PrintObjectConfig are used by infills that use Arachne (Concentric and FillEnsuring).
// Orca: also used by gap fill function.
+6 -6
View File
@@ -18,7 +18,7 @@
#include "FillGyroid.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
namespace Slic3r {
@@ -315,8 +315,8 @@ void FillGyroid::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -342,11 +342,11 @@ void FillGyroid::_fill_surface_single(
// generate pattern
Polylines polylines;
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
// Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->adaptive_infill_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
polylines = make_adaptive_tpms({gyroid, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bb, this->z, params.layer_height, this->spacing, infill_angle);
} else if (params.gyroid_optimized) {
// Marching-squares path on the gyroid implicit field. Base period matches
// the standard parametric path's wavelength: 2*pi * spacing / density_adj.
@@ -1,4 +1,4 @@
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
#include <algorithm>
#include <array>
@@ -146,11 +146,11 @@ void rasterize(const ExPolygons &expolygons, const Vec2d &origin, double cell, i
}
// Scale of the pattern relative to the surface at a depth from 0 at the surface to 1 at the deepest point.
double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
double target_scale(double ratio, TpmsAdaptiveGradient gradient, double depth)
{
switch (gradient) {
case AdaptiveInfillGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case AdaptiveInfillGradient::Exponential: return std::pow(ratio, depth);
case TpmsAdaptiveGradient::Quadratic: return 1. + (ratio - 1.) * depth * depth;
case TpmsAdaptiveGradient::Exponential: return std::pow(ratio, depth);
default: return 1. + (ratio - 1.) * depth;
}
}
@@ -159,7 +159,7 @@ double target_scale(double ratio, AdaptiveInfillGradient gradient, double depth)
// continuous level of the target of a depth.
struct DensityLevels
{
DensityLevels(double ratio, double step, AdaptiveInfillGradient gradient)
DensityLevels(double ratio, double step, TpmsAdaptiveGradient gradient)
: ratio(ratio), gradient(gradient), count(int(std::ceil(std::abs(std::log(ratio)) / std::log(step) - EPSILON)))
{}
@@ -170,7 +170,7 @@ struct DensityLevels
}
double ratio;
AdaptiveInfillGradient gradient;
TpmsAdaptiveGradient gradient;
int count;
};
@@ -212,12 +212,12 @@ private:
} // namespace
AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
TpmsRadialField::TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled)
: m_mode(mode)
, m_axis(mode == AdaptiveInfillMode::NormalX ? 0 : mode == AdaptiveInfillMode::NormalY ? 1 : mode == AdaptiveInfillMode::NormalZ ? 2 : -1)
, m_axis(mode == TpmsAdaptiveMode::NormalX ? 0 : mode == TpmsAdaptiveMode::NormalY ? 1 : mode == TpmsAdaptiveMode::NormalZ ? 2 : -1)
{
assert(!slices.empty() && mode != AdaptiveInfillMode::Disabled);
assert(!slices.empty() && mode != TpmsAdaptiveMode::Disabled);
const Vec3d min(unscaled(bbox.min.x()), unscaled(bbox.min.y()), slices.front().bottom_z);
const Vec3d extent = Vec3d(unscaled(bbox.max.x()), unscaled(bbox.max.y()), slices.back().top_z) - min;
@@ -269,7 +269,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
// Bodies are the connected inside nodes, none of which is on the border. The deepest nodes of a body are the
// centers of its lobes, unless the depth between them stays above NeckRatio; where the depth ties, the center
// is the node nearest to the middle of the tied nodes.
const bool lobes = m_mode != AdaptiveInfillMode::SteppedShells && m_mode != AdaptiveInfillMode::SmoothBlend;
const bool lobes = m_mode != TpmsAdaptiveMode::SteppedShells && m_mode != TpmsAdaptiveMode::SmoothBlend;
m_body.assign(depth.size(), -1);
std::vector<size_t> body_nodes;
for (size_t seed = 0; seed < depth.size(); ++seed) {
@@ -341,7 +341,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
}
throw_if_canceled();
if (m_mode == AdaptiveInfillMode::SteppedShells || m_mode == AdaptiveInfillMode::SmoothBlend || m_mode == AdaptiveInfillMode::DistanceWarp) {
if (m_mode == TpmsAdaptiveMode::SteppedShells || m_mode == TpmsAdaptiveMode::SmoothBlend || m_mode == TpmsAdaptiveMode::DistanceWarp) {
m_depth.assign(depth.size(), 0.f);
for (size_t i = 0; i < depth.size(); ++i)
if (m_body[i] >= 0)
@@ -421,7 +421,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
lobe.reach.resize(log_reach.size());
std::transform(log_reach.begin(), log_reach.end(), lobe.reach.begin(), [](double v) { return float(std::exp(v)); });
if (m_mode == AdaptiveInfillMode::DistanceWarp) {
if (m_mode == TpmsAdaptiveMode::DistanceWarp) {
std::vector<double> mean(log_reach.size() * (Samples + 1));
for (size_t d = 0; d < log_reach.size(); ++d) {
const Vec3d dir = direction(int(d) / Azimuth, int(d) % Azimuth);
@@ -488,7 +488,7 @@ AdaptiveInfillField::AdaptiveInfillField(const std::vector<Slice> &slices, const
}
}
double AdaptiveInfillField::depth(const Vec3d &pt) const
double TpmsRadialField::depth(const Vec3d &pt) const
{
assert(!m_depth.empty());
const Vec3d f = (pt - m_origin) / m_cell;
@@ -508,7 +508,7 @@ double AdaptiveInfillField::depth(const Vec3d &pt) const
return value;
}
Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
Vec3d TpmsRadialField::offset(const Vec3d &pt, const Vec3d &center) const
{
Vec3d d = pt - center;
if (m_axis >= 0)
@@ -516,7 +516,7 @@ Vec3d AdaptiveInfillField::offset(const Vec3d &pt, const Vec3d &center) const
return d;
}
double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
double TpmsRadialField::radial(const Lobe &lobe, const Vec3d &pt) const
{
const Vec3d d = this->offset(pt, lobe.center);
const double r = d.norm();
@@ -562,7 +562,7 @@ double AdaptiveInfillField::radial(const Lobe &lobe, const Vec3d &pt) const
return warp(mean);
}
size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
size_t TpmsRadialField::radial(const Vec3d &pt, Radials &out) const
{
assert(!this->empty());
Vec3i32 idx;
@@ -588,7 +588,7 @@ size_t AdaptiveInfillField::radial(const Vec3d &pt, Radials &out) const
return count;
}
size_t AdaptiveInfillField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
size_t TpmsRadialField::body_radial(size_t node, const Vec3d &pt, float weight, Radial *out) const
{
const Body &body = m_bodies[m_body[node]];
if (body.lobes == 1) {
@@ -641,7 +641,7 @@ struct AdaptiveTpmsField
const long gsize = std::lround(gsizef / rsizef);
const AdaptiveTpms &tpms;
const AdaptiveInfillField &radial_field;
const TpmsRadialField &radial_field;
RadialScale scale;
DensityLevels levels;
Point size;
@@ -650,7 +650,7 @@ struct AdaptiveTpmsField
double cos_angle;
double sin_angle;
AdaptiveTpmsField(const AdaptiveTpms &tpms, const AdaptiveInfillField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
AdaptiveTpmsField(const AdaptiveTpms &tpms, const TpmsRadialField &radial_field, const BoundingBox &bbox, coordf_t z, float angle)
: tpms(tpms), radial_field(radial_field), scale(tpms, radial_field.axis() < 0 ? 3 : 2)
, levels(std::max(tpms.interior_frequency / tpms.surface_frequency, 1e-3), BlendRatio, tpms.gradient)
, size(bbox.size()), offs(bbox.min), z(z)
@@ -666,7 +666,7 @@ struct AdaptiveTpmsField
const double x = unscaled(pt.x());
const double y = unscaled(pt.y());
const Vec3d obj(cos_angle * x - sin_angle * y, sin_angle * x + cos_angle * y, z);
if (radial_field.mode() == AdaptiveInfillMode::SmoothBlend) {
if (radial_field.mode() == TpmsAdaptiveMode::SmoothBlend) {
// The regular patterns of the two levels around the target of the depth, blended by a smoothstep.
auto lattice = [this, x, y](int level) {
const double frequency = tpms.surface_frequency * levels.scale(level);
@@ -681,7 +681,7 @@ struct AdaptiveTpmsField
return float((w < 1. ? (1. - w) * lattice(k) : 0.) + (w > 0. ? w * lattice(k + 1) : 0.));
}
const int axis = radial_field.axis();
AdaptiveInfillField::Radials radials;
TpmsRadialField::Radials radials;
const size_t count = radial_field.radial(obj, radials);
float value = 0.f;
for (size_t i = 0; i < count; ++i) {
@@ -708,20 +708,20 @@ template<> struct _RasterTraits<AdaptiveTpmsField>
};
// Continuous density level of the depth over a layer, in the object frame.
struct AdaptiveLevelField
struct TpmsLevelField
{
static constexpr float gsizef = 0.5f;
static constexpr float rsizef = 0.05f;
const coord_t rsize = scaled(rsizef);
const long gsize = std::lround(gsizef / rsizef);
const AdaptiveInfillField &field;
const TpmsRadialField &field;
const DensityLevels &levels;
Point size;
Point offs;
double z;
AdaptiveLevelField(const AdaptiveInfillField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
TpmsLevelField(const TpmsRadialField &field, const DensityLevels &levels, const BoundingBox &bbox, coordf_t z)
: field(field), levels(levels), size(bbox.size()), offs(bbox.min), z(z)
{}
@@ -736,19 +736,19 @@ struct AdaptiveLevelField
inline Point to_Point(const Coord &p) const { return Point(to_coord(p.c) + offs.x(), to_coord(p.r) + offs.y()); }
};
template<> struct _RasterTraits<AdaptiveLevelField>
template<> struct _RasterTraits<TpmsLevelField>
{
using ValueType = float;
static float get(const AdaptiveLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const AdaptiveLevelField &sf) { return sf.to_coordr(sf.size.x()); }
static float get(const TpmsLevelField &sf, size_t row, size_t col) { return sf.get_scalar(Coord(long(row), long(col))); }
static size_t rows(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.y()); }
static size_t cols(const TpmsLevelField &sf) { return sf.to_coordr(sf.size.x()); }
};
} // namespace marchsq
namespace Slic3r {
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle)
{
// A cell of margin for the rings closed along the raster border, and a fixed sampling grid for every region.
@@ -775,21 +775,21 @@ Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField
return polylines;
}
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, AdaptiveInfillGradient gradient)
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient)
{
const DensityLevels levels(interior_density / surface_density, ShellRatio, gradient);
if (levels.count == 0)
return {{surface_density, {expolygon}}};
// A fixed sampling grid, so that every region of a layer gets the same shells.
const coord_t cell = scaled(marchsq::AdaptiveLevelField::gsizef);
const coord_t cell = scaled(marchsq::TpmsLevelField::gsizef);
BoundingBox bbox = get_extents(expolygon);
bbox.offset(cell);
bbox.merge(align_to_grid(bbox.min, Point(cell, cell)));
const marchsq::AdaptiveLevelField raster(field, levels, bbox, z);
const marchsq::TpmsLevelField raster(field, levels, bbox, z);
// Each level takes the part deeper than the middle between it and the previous one.
std::vector<AdaptiveInfillShell> shells;
std::vector<TpmsShell> shells;
ExPolygons remaining{expolygon};
for (int level = 0; level < levels.count && !remaining.empty(); ++level) {
Polygons deeper;
@@ -808,12 +808,12 @@ std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfil
return shells;
}
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell)
{
FillParams shell_params = params;
shell_params.adaptive_infill_mode = AdaptiveInfillMode::Disabled;
for (const AdaptiveInfillShell &shell : make_adaptive_infill_shells(field, expolygon, z, params.density, params.adaptive_infill_interior_density, params.adaptive_infill_gradient)) {
shell_params.tpms_adaptive = TpmsAdaptiveMode::Disabled;
for (const TpmsShell &shell : make_tpms_shells(field, expolygon, z, params.density, params.tpms_interior_density, params.tpms_adaptive_gradient)) {
shell_params.density = shell.density;
for (const ExPolygon &part : offset_ex(shell.expolygons, -float(scale_(0.5 * spacing))))
fill_shell(shell_params, part);
@@ -21,7 +21,7 @@ namespace Slic3r {
// lobe, 1 at its surface. Lobes are parts of a body separated by a neck, like two spheres united. In the 2D modes,
// every section normal to the axis has its own bodies and lobes, and distances are measured within the section. The
// modes following the distance to the surface use the depth of every point instead; Distance warp also the lobes.
class AdaptiveInfillField
class TpmsRadialField
{
public:
struct Slice
@@ -39,7 +39,7 @@ public:
};
// Slices sorted by z, in the XY coordinates of the fill and the print Z.
AdaptiveInfillField(const std::vector<Slice> &slices, const BoundingBox &bbox, AdaptiveInfillMode mode,
TpmsRadialField(const std::vector<Slice> &slices, const BoundingBox &bbox, TpmsAdaptiveMode mode,
const std::function<void()> &throw_if_canceled);
// Lobes blended near the sides between them, from a body or from each of the two sections around a point.
@@ -56,7 +56,7 @@ public:
// Axis normal to the sections in the 2D modes, -1 in the modes graded in 3D.
int axis() const { return m_axis; }
AdaptiveInfillMode mode() const { return m_mode; }
TpmsAdaptiveMode mode() const { return m_mode; }
// In the modes following the distance to the surface: depth relative to the deepest point of the body, from 0 at
// the surface to 1.
@@ -91,7 +91,7 @@ private:
static constexpr int Polar = 24;
static constexpr int Azimuth = 48;
AdaptiveInfillMode m_mode;
TpmsAdaptiveMode m_mode;
int m_axis;
Vec3d m_origin;
double m_cell;
@@ -106,9 +106,9 @@ private:
std::vector<float> m_depth;
};
using AdaptiveInfillFieldPtr = std::unique_ptr<AdaptiveInfillField>;
using TpmsRadialFieldPtr = std::unique_ptr<TpmsRadialField>;
// A field for every adaptive mode in use, indexed by the mode.
using AdaptiveInfillFields = std::array<AdaptiveInfillFieldPtr, size_t(AdaptiveInfillMode::Count)>;
using TpmsRadialFields = std::array<TpmsRadialFieldPtr, size_t(TpmsAdaptiveMode::Count)>;
struct AdaptiveTpms
{
@@ -117,14 +117,14 @@ struct AdaptiveTpms
// Pattern frequencies at the surface and at the center, in radians per mm.
double surface_frequency;
double interior_frequency;
AdaptiveInfillGradient gradient;
TpmsAdaptiveGradient gradient;
};
// Infill lines in the fill frame, the object frame rotated by -angle; z is the print_z of the layer.
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const AdaptiveInfillField &field, BoundingBox bbox,
Polylines make_adaptive_tpms(const AdaptiveTpms &tpms, const TpmsRadialField &field, BoundingBox bbox,
coordf_t z, coordf_t layer_height, coordf_t spacing, float angle);
struct AdaptiveInfillShell
struct TpmsShell
{
float density;
ExPolygons expolygons;
@@ -132,12 +132,12 @@ struct AdaptiveInfillShell
// Stepped shells: the parts of an expolygon in the object frame at each density, from the surface inwards; z is the
// middle of the layer.
std::vector<AdaptiveInfillShell> make_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, AdaptiveInfillGradient gradient);
std::vector<TpmsShell> make_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z,
float surface_density, float interior_density, TpmsAdaptiveGradient gradient);
// Stepped shells: fills every shell with fill_shell at its density, each shrunk by half a line like a filled region,
// so the lines connected along the boundaries of two shells don't overlap.
void fill_adaptive_infill_shells(const AdaptiveInfillField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
void fill_tpms_shells(const TpmsRadialField &field, const ExPolygon &expolygon, coordf_t z, const FillParams &params, coordf_t spacing,
const std::function<void(const FillParams &, const ExPolygon &)> &fill_shell);
} // namespace Slic3r
+6 -6
View File
@@ -17,7 +17,7 @@
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include "FillTpmsD.hpp"
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
namespace Slic3r {
@@ -117,8 +117,8 @@ void FillTpmsD::_fill_surface_single(
ExPolygon expolygon,
Polylines &polylines_out)
{
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -130,13 +130,13 @@ void FillTpmsD::_fill_surface_single(
expolygon.rotate(-infill_angle);
Polylines polylines;
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
// Radians per mm of the regular pattern at a density.
auto frequency = [&params, this](double density) { return density * DensityAdjust / (params.multiline * this->spacing); };
BoundingBox bbox = expolygon.contour.bounding_box();
bbox.offset(scale_((params.multiline + 1) * this->spacing));
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.adaptive_infill_interior_density), params.adaptive_infill_gradient},
*this->adaptive_infill_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
polylines = make_adaptive_tpms({schwarz_d, frequency(params.density), frequency(params.tpms_interior_density), params.tpms_adaptive_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, this->spacing, infill_angle);
} else {
BoundingBox bb = expolygon.contour.bounding_box();
// Density adjusted to have a good %of weight.
+7 -7
View File
@@ -8,7 +8,7 @@
#include "libslic3r/Fill/FillBase.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "FillTpmsFK.hpp"
#include "AdaptiveInfillField.hpp"
#include "FillTpmsAdaptive.hpp"
#include <cmath>
#include <algorithm>
#include <cstddef>
@@ -132,8 +132,8 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
ExPolygon expolygon,
Polylines& polylines_out)
{
if (params.adaptive_infill_mode == AdaptiveInfillMode::SteppedShells && this->adaptive_infill_field != nullptr) {
fill_adaptive_infill_shells(*this->adaptive_infill_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
if (params.tpms_adaptive == TpmsAdaptiveMode::SteppedShells && this->tpms_radial_field != nullptr) {
fill_tpms_shells(*this->tpms_radial_field, expolygon, this->z - 0.5 * params.layer_height, params, this->spacing,
[&](const FillParams &shell_params, const ExPolygon &shell) {
this->_fill_surface_single(shell_params, thickness_layers, direction, shell, polylines_out);
});
@@ -151,10 +151,10 @@ void FillTpmsFK::_fill_surface_single(const FillParams& params,
// Enlarge the bounding box by the multi-line width to avoid artifacts at the edges.
bbox.offset(scale_((params.multiline + 1) * spacing));
Polylines polylines;
if (params.adaptive_infill_mode != AdaptiveInfillMode::Disabled && this->adaptive_infill_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.adaptive_infill_interior_density),
params.adaptive_infill_gradient},
*this->adaptive_infill_field, bbox, this->z, params.layer_height, spacing, infill_angle);
if (params.tpms_adaptive != TpmsAdaptiveMode::Disabled && this->tpms_radial_field != nullptr) {
polylines = make_adaptive_tpms({fischer_koch, 2. * PI / period(params.density), 2. * PI / period(params.tpms_interior_density),
params.tpms_adaptive_gradient},
*this->tpms_radial_field, bbox, this->z, params.layer_height, spacing, infill_angle);
} else {
marchsq::ScalarField sf = marchsq::ScalarField(bbox, this->z, period(params.density));
// Get simplified lines using coarse tolerance of 0.1mm (this is infill).
+5 -78
View File
@@ -6,7 +6,6 @@
#include "../ShortestPath.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/libslic3r.h"
#include <algorithm>
#include <cmath>
@@ -141,87 +140,15 @@ bool tsp_remove_crossings(std::vector<size_t>& path, const Points& centers)
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// For many islands, the same scan with the edges binned in a uniform grid over their boxes, so each edge is only tested against the edges sharing a
// cell with it - two edges whose boxes overlap always do. It returns the same crossing as the all-pairs scan
// (smallest i, then smallest j), so the result is unchanged. The all-pairs scan is quadratic in the edge count and
// runs again after every reversal; rebuilding the grid costs more than it saves below the threshold.
constexpr size_t grid_min_size = 500;
BoundingBox extent;
for (size_t idx : path)
extent.merge(centers[idx]);
const int grid_n = std::clamp(int(std::sqrt(double(pn))), 1, 256);
const coord_t cell_w = std::max<coord_t>(1, (extent.max.x() - extent.min.x()) / grid_n + 1);
const coord_t cell_h = std::max<coord_t>(1, (extent.max.y() - extent.min.y()) / grid_n + 1);
const auto for_cells = [&](const Point& a, const Point& b, auto&& fn) {
const int x0 = int((std::min(a.x(), b.x()) - extent.min.x()) / cell_w), x1 = int((std::max(a.x(), b.x()) - extent.min.x()) / cell_w);
const int y0 = int((std::min(a.y(), b.y()) - extent.min.y()) / cell_h), y1 = int((std::max(a.y(), b.y()) - extent.min.y()) / cell_h);
for (int y = y0; y <= y1; ++y)
for (int x = x0; x <= x1; ++x)
fn(y * grid_n + x);
};
std::vector<std::vector<size_t>> edge_cells(size_t(grid_n) * grid_n);
auto find_crossing_grid = [&]() -> std::pair<size_t, size_t> {
for (std::vector<size_t>& cell : edge_cells)
cell.clear();
for (size_t j = 0; j < n_edges; ++j)
for_cells(centers[path[j]], centers[path[(j + 1) % pn]], [&](int cell) { edge_cells[cell].emplace_back(j); });
for (size_t i = 0; i < n_edges; ++i) {
const Point& ai = centers[path[i]];
const Point& bi = centers[path[(i + 1) % pn]];
size_t first_j = std::numeric_limits<size_t>::max();
for_cells(ai, bi, [&](int cell) {
for (size_t j : edge_cells[cell]) {
if (j < i + 2 || j >= first_j) continue;
// Skip the (0, pn-1) pair: edges (0,1) and (pn-1,0) share node 0.
if (i == 0 && j == pn - 1) continue;
const Point& aj = centers[path[j]];
const Point& bj = centers[path[(j + 1) % pn]];
if (!bboxes_overlap(ai, bi, aj, bj)) continue;
if (Geometry::segments_intersect(ai, bi, aj, bj))
first_j = j;
}
});
if (first_j != std::numeric_limits<size_t>::max())
return {i, first_j};
}
return {std::numeric_limits<size_t>::max(), std::numeric_limits<size_t>::max()};
};
// Process crossings one at a time: find first, reverse it, restart scan.
// Cap iterations to prevent infinite loops on collinear/overlapping segments.
const int max_iters = static_cast<int>(pn * pn);
int max_iters = static_cast<int>(pn * pn);
bool improved = false;
// Reversing between two segments that only touch or overlap along a line need not remove the intersection, so on
// islands laid out on a regular grid (a tiled texture, an array of parts) the loop can cycle through the same
// orderings until the pn * pn cap. Once an ordering repeats the rest of the loop is periodic, so only the steps
// to the ordering the capped loop would have stopped on are taken.
std::unordered_map<uint64_t, int> seen_paths; // path hash -> reversals done when it was reached
const auto path_hash = [&path]() {
uint64_t h = 1469598103934665603ull; // FNV-1a
for (size_t idx : path)
h = (h ^ uint64_t(idx)) * 1099511628211ull;
return h;
};
const auto reverse_first_crossing = [&]() {
auto [ci, cj] = pn >= grid_min_size ? find_crossing_grid() : find_crossing();
if (ci == std::numeric_limits<size_t>::max())
return false;
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
return true;
};
seen_paths.emplace(path_hash(), 0);
for (int iter = 1; iter <= max_iters && reverse_first_crossing(); ++iter) {
while (max_iters-- > 0) {
auto [ci, cj] = find_crossing();
if (ci == std::numeric_limits<size_t>::max()) break;
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;
}
std::reverse(path.begin() + ci + 1, path.begin() + cj + 1);
}
return improved;
}
+13 -18
View File
@@ -29,7 +29,6 @@
#include <memory>
#include <random>
#include <algorithm>
#include <limits>
#include <queue>
#include <string>
#include <unordered_map>
@@ -1208,21 +1207,21 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
const size_t layer_idx, const float max_distance,
const SeamPlacerImpl::SeamComparator &comparator) const {
using namespace SeamPlacerImpl;
// Find the best nearby point and the nearest one. A layer of a fine relief has tens of thousands of candidates within
// the radius, so they are looked at as the search finds them rather than collected into a vector first.
constexpr size_t none = std::numeric_limits<size_t>::max();
size_t best_nearby_point_index = none;
size_t nearest_point_index = none;
visit_nearby_points(*layers[layer_idx].points_tree, projected_position, max_distance,
[&layers, &comparator, &projected_position, layer_idx, &best_nearby_point_index, &nearest_point_index]
(size_t nearby_point_index) {
if (best_nearby_point_index == none) {
// The first point found starts both, as the first of the collected ones did.
best_nearby_point_index = nearest_point_index = nearby_point_index;
}
std::vector<size_t> nearby_points_indices = find_nearby_points(*layers[layer_idx].points_tree, projected_position,
max_distance);
if (nearby_points_indices.empty()) {
return {};
}
size_t best_nearby_point_index = nearby_points_indices[0];
size_t nearest_point_index = nearby_points_indices[0];
// Now find best nearby point, nearest point, and corresponding indices
for (const size_t &nearby_point_index : nearby_points_indices) {
const SeamCandidate &point = layers[layer_idx].points[nearby_point_index];
if (point.perimeter.finalized) {
return; // skip over finalized perimeters, try to find some that is not finalized
continue; // skip over finalized perimeters, try to find some that is not finalized
}
if (comparator.is_first_better(point, layers[layer_idx].points[best_nearby_point_index],
projected_position.head<2>())
@@ -1234,10 +1233,6 @@ std::optional<std::pair<size_t, size_t>> SeamPlacer::find_next_seam_in_layer(
|| layers[layer_idx].points[nearest_point_index].perimeter.finalized) {
nearest_point_index = nearby_point_index;
}
});
if (best_nearby_point_index == none) {
return {};
}
const SeamCandidate &best_nearby_point = layers[layer_idx].points[best_nearby_point_index];
-30
View File
@@ -318,36 +318,6 @@ std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const P
return visitor.result;
}
// Visits the points within max_distance of center, in the order find_nearby_points() would collect them, and hands
// each of them to `visitor_fn` instead of returning them all: a search over a dense set spends more on collecting the
// points into a vector than on the search itself, and its caller usually keeps only a few of them.
template<typename KDTreeIndirectType, typename PointType, typename VisitorFn>
void visit_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType &max_distance, VisitorFn visitor_fn)
{
using CoordType = typename KDTreeIndirectType::CoordType;
struct Visitor {
const KDTreeIndirectType &kdtree;
const PointType center;
const CoordType max_distance_squared;
VisitorFn visitor_fn;
unsigned int operator()(size_t idx, size_t dimension) {
auto dist = CoordType(0);
for (size_t i = 0; i < KDTreeIndirectType::NumDimensions; ++i) {
CoordType d = center[i] - kdtree.coordinate(idx, i);
dist += d * d;
}
if (dist < max_distance_squared)
visitor_fn(idx);
return kdtree.descent_mask(center[dimension], max_distance_squared, idx, dimension);
}
} visitor { kdtree, center, max_distance * max_distance, visitor_fn };
kdtree.visit(visitor);
}
template<typename KDTreeIndirectType, typename PointType>
std::vector<size_t> find_nearby_points(const KDTreeIndirectType &kdtree, const PointType &center,
const typename KDTreeIndirectType::CoordType& max_distance)
+2 -2
View File
@@ -443,8 +443,8 @@ coordf_t Layer::get_sparse_infill_max_void_area()
if (density == 0.)
return -1;
// Orca: the adaptive TPMS infill is as sparse as its interior density.
if (density < 100.f && config.adaptive_infill_mode != AdaptiveInfillMode::Disabled && is_adaptive_infill_pattern(pattern))
density = std::min(density, std::max(1.f, float(config.adaptive_infill_interior_density)));
if (density < 100.f && config.tpms_adaptive != TpmsAdaptiveMode::Disabled && is_tpms_adaptive_pattern(pattern))
density = std::min(density, std::max(1.f, float(config.tpms_interior_density)));
//BBS: rough estimation and need to be optimized
double spacing = flow.scaled_spacing() * (100 - density) / density;
+1 -2
View File
@@ -99,11 +99,10 @@ void LayerRegion::slices_to_fill_surfaces_clipped()
by_surface[size_t(surface.surface_type)].emplace_back(&surface);
// Trim surfaces by the fill_boundaries.
this->fill_surfaces.surfaces.clear();
const Polygons fill_boundaries = to_polygons(this->fill_expolygons);
for (size_t surface_type = 0; surface_type < size_t(stCount); ++ surface_type) {
const SurfacesPtr &this_surfaces = by_surface[surface_type];
if (! this_surfaces.empty())
this->fill_surfaces.append(intersection_ex_by_piece(to_expolygons(this_surfaces), fill_boundaries), SurfaceType(surface_type));
this->fill_surfaces.append(intersection_ex(this_surfaces, this->fill_expolygons), SurfaceType(surface_type));
}
}
+72 -272
View File
@@ -22,7 +22,6 @@
#include "Surface.hpp"
#include "libslic3r.h"
#include <numeric>
#include <cmath>
#include <cstddef>
#include <list>
@@ -1362,15 +1361,10 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
}
#endif // MM_SEGMENTATION_DEBUG_TOP_BOTTOM
// When the upper surface of an object is occluded, it should no longer be considered the upper surface.
// 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.
// When the upper surface of an object is occluded, it should no longer be considered the upper surface
{
const size_t occluded_pairs = num_facets_states * layers.size();
tbb::parallel_for(tbb::blocked_range<size_t>(0, occluded_pairs), [&](const tbb::blocked_range<size_t> &range) {
for (size_t pair_idx = range.begin(); pair_idx < range.end(); ++pair_idx) {
const size_t extruder_idx = pair_idx / layers.size();
const size_t layer_idx = pair_idx % layers.size();
for (size_t extruder_idx = 0; extruder_idx < num_facets_states; ++extruder_idx) {
for (size_t layer_idx = 0; layer_idx < layers.size(); ++layer_idx) {
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]);
}
@@ -1378,7 +1372,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_raw[extruder_idx][layer_idx] = diff(bottom_raw[extruder_idx][layer_idx], input_expolygons[layer_idx - 1]);
}
}
});
}
}
std::vector<std::vector<ExPolygons>> triangles_by_color_bottom(num_facets_states);
@@ -1436,58 +1430,11 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
return out;
};
// Projects a painted top or bottom face `ex` of layer `layer_idx` onto the shell layers below or above it (in
// `shell_layers`, nearest first), one more perimeter in on each, stopping at the first layer where nothing is left.
// Only the slices within the deepest offset of `ex` (three times that with the miter joins) decide the result, so the
// work is done per tile of `ex`'s ExPolygons on the slices cut to the tile's box grown by that much: the same result, but
// each Clipper call stays the size of a tile rather than of a layer cut through a fine relief, and the tiles run in
// parallel.
const auto project_to_shells = [&input_expolygons](const ExPolygons &ex, size_t layer_idx, const std::vector<size_t> &shell_layers,
const LayerColorStat &stat, ShellProjections &dst) {
std::vector<float> offsets(shell_layers.size());
float offset = 0.f;
for (size_t i = 0; i < shell_layers.size(); ++i) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
offset -= (stat.extrusion_spacing + stat.extrusion_width);
offsets[i] = offset;
}
if (offsets.empty())
return;
const coord_t reach = coord_t(std::ceil(DefaultMiterLimit * std::abs(offsets.back()))) + 10 * SCALED_EPSILON;
const std::vector<ClipperUtils::ExPolygonsTile> tiles = ClipperUtils::tile_expolygons(ex, 16);
// [shell layer][tile]
std::vector<std::vector<ExPolygons>> shells(shell_layers.size(), std::vector<ExPolygons>(tiles.size()));
tbb::parallel_for(size_t(0), tiles.size(), [&](size_t tile_idx) {
const ClipperUtils::ExPolygonsTile &tile = tiles[tile_idx];
const BoundingBox bbox = tile.bbox.inflated(reach);
ExPolygons tile_ex;
tile_ex.reserve(tile.members.size());
for (size_t i : tile.members)
tile_ex.emplace_back(ex[i]);
Polygons layer_slices_trimmed = ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[layer_idx], bbox);
for (size_t i = 0; i < shell_layers.size() && ! layer_slices_trimmed.empty(); ++i) {
const ExPolygons trimmed = intersection_ex(layer_slices_trimmed, ClipperUtils::clip_clipper_polygons_with_subject_bbox(input_expolygons[shell_layers[i]], bbox));
shells[i][tile_idx] = opening_ex(intersection_ex(tile_ex, offset_ex(trimmed, offsets[i])), stat.small_region_threshold);
layer_slices_trimmed = to_polygons(trimmed);
}
});
for (size_t i = 0; i < shell_layers.size(); ++i) {
ExPolygons shell;
for (ExPolygons &tile_shell : shells[i])
append(shell, std::move(tile_shell));
if (shell.empty())
break;
dst.emplace_back(shell_layers[i], std::move(shell));
}
};
tbb::parallel_for(tbb::blocked_range<size_t>(0, num_layers), [&num_layers, &num_facets_states, &layer_color_stat, &top_raw, &triangles_by_color_top,
&throw_on_cancel_callback, &bottom_raw, &triangles_by_color_bottom, &project_to_shells,
&throw_on_cancel_callback, &input_expolygons, &bottom_raw, &triangles_by_color_bottom,
&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) {
// 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) {
for (size_t color_idx = 0; color_idx < num_facets_states; ++color_idx) {
throw_on_cancel_callback();
LayerColorStat stat = layer_color_stat(layer_idx, color_idx);
if (std::vector<Polygons> &top = top_raw[color_idx]; ! top.empty() && ! top[layer_idx].empty())
@@ -1496,10 +1443,18 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
top_ex = opening_ex(top_ex, stat.small_region_threshold);
if (! top_ex.empty()) {
append(triangles_by_color_top[color_idx][layer_idx], top_ex);
std::vector<size_t> shell_layers;
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx)
shell_layers.emplace_back(size_t(last_idx));
project_to_shells(top_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_top[color_idx][layer_idx]);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (int last_idx = int(layer_idx) - 1; last_idx > std::max(int(layer_idx - stat.top_shell_layers), int(0)); --last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width ;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(top_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_top[color_idx][layer_idx].emplace_back(size_t(last_idx), std::move(last));
}
}
}
if (std::vector<Polygons> &bottom = bottom_raw[color_idx]; ! bottom.empty() && ! bottom[layer_idx].empty())
@@ -1508,13 +1463,21 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
bottom_ex = opening_ex(bottom_ex, stat.small_region_threshold);
if (! bottom_ex.empty()) {
append(triangles_by_color_bottom[color_idx][layer_idx], bottom_ex);
std::vector<size_t> shell_layers;
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx)
shell_layers.emplace_back(last_idx);
project_to_shells(bottom_ex, layer_idx, shell_layers, stat, shell_triangles_by_color_bottom[color_idx][layer_idx]);
float offset = 0.f;
ExPolygons layer_slices_trimmed = input_expolygons[layer_idx];
for (size_t last_idx = layer_idx + 1; last_idx < std::min(layer_idx + stat.bottom_shell_layers, num_layers); ++last_idx) {
//BBS: offset width should be 2*spacing to avoid too narrow area which has overlap of wall line
//offset -= stat.extrusion_width;
offset -= (stat.extrusion_spacing + stat.extrusion_width);
layer_slices_trimmed = intersection_ex(layer_slices_trimmed, input_expolygons[last_idx]);
ExPolygons last = opening_ex(intersection_ex(bottom_ex, offset_ex(layer_slices_trimmed, offset)), stat.small_region_threshold);
if (last.empty())
break;
shell_triangles_by_color_bottom[color_idx][layer_idx].emplace_back(last_idx, std::move(last));
}
}
}
});
}
}
});
@@ -1535,23 +1498,20 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
&shell_top_by_layer, &shell_bottom_by_layer](const tbb::blocked_range<size_t> &range) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) {
throw_on_cancel_callback();
// The per-colour unions below are independent of each other, so they run in parallel (a painted top or
// bottom face puts all of its colours on one layer); whatever combines the colours stays in colour order.
const auto merge_colour_union = [&](size_t color_idx) {
ExPolygons painted_exploys;
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx];
append(self, std::move(triangles_by_color_bottom[color_idx][layer_idx]));
append(self, std::move(triangles_by_color_top[color_idx][layer_idx]));
self = union_ex(self);
};
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), merge_colour_union);
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]);
append(painted_exploys, self);
}
painted_exploys = union_ex(painted_exploys);
//BBS: merge the top and bottom shell layers
tbb::parallel_for(size_t(0), triangles_by_color_merged.size(), [&](size_t color_idx) {
for (size_t color_idx = 0; color_idx < triangles_by_color_merged.size(); ++color_idx) {
auto &self = triangles_by_color_merged[color_idx][layer_idx];
auto top_area = diff_ex(union_ex(shell_top_by_layer[color_idx][layer_idx]), painted_exploys);
@@ -1560,7 +1520,7 @@ static inline std::vector<std::vector<ExPolygons>> segmentation_top_and_bottom_l
append(self, top_area);
append(self, bottom_area);
self = union_ex(self);
});
}
// Trim one region by the other if some of the regions overlap.
ExPolygons painted_regions;
for (size_t color_idx = 1; color_idx < triangles_by_color_merged.size(); ++color_idx) {
@@ -1927,69 +1887,7 @@ static void remove_multiple_edges_in_vertices(MMU_Graph &graph, const std::vecto
}
}
// 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,
static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::vector<std::vector<ExPolygons>> &segmented_regions,
std::vector<std::vector<ExPolygons>> &&top_and_bottom_layers,
const size_t num_facets_states,
const std::function<void()> &throw_on_cancel_callback)
@@ -2000,91 +1898,33 @@ static std::vector<std::vector<ExPolygons>> merge_segmented_layers(const std::ve
assert(!top_and_bottom_layers.size() || num_facets_states == top_and_bottom_layers.size());
BOOST_LOG_TRIVIAL(debug) << "Print object segmentation - Merging segmented layers in parallel - Begin";
// 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) {
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) {
for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++layer_idx) {
assert(segmented_regions[layer_idx].size() == num_facets_states);
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.
// Zero is skipped because it is the default color of the volume
for (size_t extruder_id = 1; extruder_id < num_facets_states; ++extruder_id) {
if (top_and_bottom_layers.empty() || top_and_bottom_layers[extruder_id][layer_idx].empty()) {
for (size_t bucket = 0; bucket < num_buckets; ++bucket)
append(segmented_regions_merged[layer_idx][extruder_id - 1], std::move(merged[bucket][extruder_id]));
continue;
throw_on_cancel_callback();
if (!segmented_regions[layer_idx][extruder_id].empty()) {
ExPolygons segmented_regions_trimmed = segmented_regions[layer_idx][extruder_id];
if (!top_and_bottom_layers.empty()) {
for (const std::vector<ExPolygons> &top_and_bottom_by_extruder : top_and_bottom_layers) {
if (!top_and_bottom_by_extruder[layer_idx].empty() && !segmented_regions_trimmed.empty()) {
segmented_regions_trimmed = diff_ex(segmented_regions_trimmed, top_and_bottom_by_extruder[layer_idx]);
}
}
}
segmented_regions_merged[layer_idx][extruder_id - 1] = std::move(segmented_regions_trimmed);
}
if (!top_and_bottom_layers.empty() && !top_and_bottom_layers[extruder_id][layer_idx].empty()) {
bool was_top_and_bottom_empty = segmented_regions_merged[layer_idx][extruder_id - 1].empty();
append(segmented_regions_merged[layer_idx][extruder_id - 1], top_and_bottom_layers[extruder_id][layer_idx]);
// Remove dimples (#7235) appearing after merging side segmentation of the model with tops and bottoms painted layers.
if (!was_top_and_bottom_empty)
segmented_regions_merged[layer_idx][extruder_id - 1] = offset2_ex(union_ex(segmented_regions_merged[layer_idx][extruder_id - 1]), float(SCALED_EPSILON), -float(SCALED_EPSILON));
}
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
@@ -2372,56 +2212,16 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
assert(!color_poly.empty());
assert(!color_poly.front().empty());
// Each island (an ExPolygon with its holes) is segmented on its own. Any point of an island is closer to
// that island's contours than to any other island's - the way out crosses its own boundary first - so its
// Voronoi cells, and with them its colour regions, depend on nothing else. A layer cut through a fine relief
// has thousands of islands, and one Voronoi diagram over all of them degenerated into overlapping regions
// that every boolean afterwards had to untangle. Per island the diagrams stay small and the islands run in
// parallel; an island in a single colour needs no diagram at all.
const ExPolygons &islands = input_expolygons[layer_idx];
std::vector<std::pair<size_t, size_t>> island_contours(islands.size()); // [first, last) into color_poly
{
// The same order EdgeGrid::Grid::create() lists the contours in, and so colorize_contours().
size_t idx = 0;
for (size_t island_idx = 0; island_idx < islands.size(); ++island_idx) {
const size_t first = idx;
if (!islands[island_idx].contour.empty())
++idx;
for (const Polygon &hole : islands[island_idx].holes)
if (!hole.empty())
++idx;
island_contours[island_idx] = {first, idx};
}
assert(idx == color_poly.size());
if (has_layer_only_one_color(color_poly)) {
// If the whole layer is painted using the same color, it is not needed to construct a Voronoi diagram for the segmentation of this layer.
segmented_regions[layer_idx][size_t(color_poly.front().front().color)] = input_expolygons[layer_idx];
} else {
MMU_Graph graph = build_graph(layer_idx, color_poly);
remove_multiple_edges_in_vertices(graph, color_poly);
graph.remove_nodes_with_one_arc();
segmented_regions[layer_idx] = extract_colored_segments(graph, num_facets_states);
//segmented_regions[layer_idx] = extract_colored_segments(color_poly, num_extruders, layer_idx);
}
std::vector<std::vector<ExPolygons>> island_regions(islands.size());
tbb::parallel_for(size_t(0), islands.size(), [&](size_t island_idx) {
const auto [first, last] = island_contours[island_idx];
if (first == last)
return;
const std::vector<ColoredLines> island_poly(color_poly.begin() + first, color_poly.begin() + last);
std::vector<ExPolygons> &regions = island_regions[island_idx];
if (has_layer_only_one_color(island_poly)) {
regions.assign(num_facets_states, ExPolygons());
regions[size_t(island_poly.front().front().color)].emplace_back(islands[island_idx]);
} else {
MMU_Graph graph = build_graph(layer_idx, island_poly);
remove_multiple_edges_in_vertices(graph, island_poly);
graph.remove_nodes_with_one_arc();
regions = extract_colored_segments(graph, num_facets_states);
// The faces of one colour tile it without overlapping; merged here, where an island is small,
// every later boolean gets a few regions instead of thousands of faces sharing their edges. An
// island with many holes keeps its faces: merged, each colour would be one region with thousands
// of holes, and subtracting from that is far slower than from the faces one at a time.
if (island_poly.size() <= 64)
for (ExPolygons &faces : regions)
if (faces.size() > 1)
faces = union_ex(faces);
}
});
for (std::vector<ExPolygons> &regions : island_regions)
for (size_t color_idx = 0; color_idx < regions.size(); ++color_idx)
append(segmented_regions[layer_idx][color_idx], std::move(regions[color_idx]));
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
export_regions_to_svg(debug_out_path("3-mm-regions-sides-%d-%d.svg", layer_idx, iRun), segmented_regions[layer_idx], input_expolygons[layer_idx]);
@@ -2444,7 +2244,7 @@ std::vector<std::vector<ExPolygons>> segmentation_by_painting(const PrintObject
throw_on_cancel_callback();
}
std::vector<std::vector<ExPolygons>> segmented_regions_merged = merge_segmented_layers(input_expolygons, 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(segmented_regions, std::move(top_and_bottom_layers), num_facets_states, throw_on_cancel_callback);
throw_on_cancel_callback();
#ifdef MM_SEGMENTATION_DEBUG_REGIONS
+2 -6
View File
@@ -23,15 +23,11 @@ public:
MultiPoint() {}
MultiPoint(const MultiPoint &other) : points(other.points) {}
MultiPoint(MultiPoint &&other) noexcept : points(std::move(other.points)) {}
MultiPoint(MultiPoint &&other) : points(std::move(other.points)) {}
MultiPoint(std::initializer_list<Point> list) : points(list) {}
explicit MultiPoint(const Points &_points) : points(_points) {}
// Without it, the derived classes' move constructors passing std::move(points) here copied them, which
// also means a moved-from Polygon or Polyline is now really empty where it used to silently keep its
// points: a use-after-move anywhere in the tree that happened to work before now sees nothing.
explicit MultiPoint(Points &&_points) noexcept : points(std::move(_points)) {}
MultiPoint& operator=(const MultiPoint &other) { points = other.points; return *this; }
MultiPoint& operator=(MultiPoint &&other) noexcept { points = std::move(other.points); return *this; }
MultiPoint& operator=(MultiPoint &&other) { points = std::move(other.points); return *this; }
virtual ~MultiPoint() = default;
void scale(double factor);
void scale(double factor_x, double factor_y);
+401 -426
View File
@@ -32,8 +32,6 @@
#include <tuple>
#include <unordered_set>
#include <thread>
#include <tbb/blocked_range.h>
#include <tbb/parallel_for.h>
#include <vector>
#include "libslic3r.h"
#include <utility>
@@ -2585,490 +2583,467 @@ void PerimeterGenerator::process_arachne()
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
// we need to process each island separately because we might have different
// extra perimeters for each one
// Each island is independent up to its outputs, so they are generated in parallel - a layer split into thousands
// of islands (e.g. by colour painting) otherwise ran on one thread - and the outputs are then committed in the
// original island order, which is what the extra overhang perimeters (applied to the last island's loops and to
// all fill surfaces so far) depend on.
struct ArachneSurfaceResult
{
ExtrusionEntityCollection loops;
bool has_loops = false;
ExPolygons infill;
ExPolygons no_overlap;
};
std::vector<ArachneSurfaceResult> results(all_surfaces.size());
tbb::parallel_for(tbb::blocked_range<size_t>(0, all_surfaces.size()), [&](const tbb::blocked_range<size_t> &range) {
for (size_t surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) {
const Surface &surface = all_surfaces[surface_idx];
ArachneSurfaceResult &result = results[surface_idx];
coord_t bead_width_0 = ext_perimeter_spacing;
// detect how many perimeters must be generated for this island
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
for (const Surface& surface : all_surfaces) {
coord_t bead_width_0 = ext_perimeter_spacing;
// detect how many perimeters must be generated for this island
int loop_number = this->config->wall_loops + surface.extra_perimeters - 1; // 0-indexed loops
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
// Orca: properly adjust offset for the outer wall if precise_outer_wall is enabled.
ExPolygons last = offset_ex(surface.expolygon.simplify_p(surface_simplify_resolution),
apply_precise_outer_wall? -float(ext_perimeter_width - ext_perimeter_spacing )
: -float(ext_perimeter_width / 2. - ext_perimeter_spacing / 2.));
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
Arachne::WallToolPathsParams input_params = Arachne::make_paths_params(this->layer_id, *object_config, *print_config);
// Set params is_top_or_bottom_layer for adjusting short-wall removal sensitivity.
input_params.is_top_or_bottom_layer = (is_bottom_layer || is_topmost_layer) ? true : false;
coord_t wall_0_inset = 0;
if (apply_precise_outer_wall)
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
coord_t wall_0_inset = 0;
if (apply_precise_outer_wall)
wall_0_inset = -coord_t(ext_perimeter_width / 2 - ext_perimeter_spacing / 2);
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
// Set one perimeter when TopSurfaces is selected.
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
Arachne::WallToolPathsParams input_params_tmp = input_params;
Arachne::WallToolPathsParams input_params_tmp = input_params;
Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
wall_0_inset, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
Polygons last_p = to_polygons(last);
Arachne::WallToolPaths wallToolPaths(last_p, bead_width_0, perimeter_spacing, coord_t(loop_number + 1),
wall_0_inset, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> perimeters = wallToolPaths.getToolPaths();
ExPolygons infill_contour = union_ex(wallToolPaths.getInnerContour());
// Check if there are some remaining perimeters to generate (the number of perimeters
// is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
// Check if there are some remaining perimeters to generate (the number of perimeters
// is greater than one together with enabled the single perimeter on top surface feature).
if (inner_loop_number >= 0) {
assert(upper_slices != nullptr);
coord_t perimeter_width = this->perimeter_flow.scaled_width();
coord_t perimeter_width = this->perimeter_flow.scaled_width();
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Filter out areas that are too thin and expand top surface polygons a bit to hide the wall line.
// ORCA: skip if the top surface area is smaller than "min_width_top_surface"
const float top_surface_min_width = std::max<float>(float(ext_perimeter_spacing) / 4.f + scaled<float>(0.00001), float(scale_(config->min_width_top_surface.get_abs_value(unscale_(perimeter_width)))) / 4.f);
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
// contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box.
BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON);
// Get top ExPolygons from the given contour. uncovered reports whether the upper layer leaves any of the
// contour uncovered, before bridges and too thin areas are filtered out.
auto get_top_expolygons = [&](const ExPolygons &contour, bool &uncovered) {
// Contour bounding box.
BoundingBox contour_bbox = get_extents(contour);
contour_bbox.offset(SCALED_EPSILON);
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
Polygons upper_slices_clipped;
if (object_config->interface_shells) {
auto upper_slicer_same_region = to_expolygons(this->upper_slices_same_region->surfaces);
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(upper_slicer_same_region, contour_bbox);
} else
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, contour_bbox);
ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty();
if (top.empty())
return top;
ExPolygons top = diff_ex(contour, upper_slices_clipped);
uncovered = !top.empty();
if (top.empty())
return top;
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
if (lower_slices != nullptr) {
const float bridge_offset = float(std::max<coord_t>(ext_perimeter_spacing, perimeter_width));
const Polygons lower_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*lower_slices, contour_bbox);
const ExPolygons current_slices_bridges = offset_ex(diff_ex(top, lower_slices_clipped), bridge_offset);
// Remove bridges from top surface polygons.
top = diff_ex(top, current_slices_bridges);
}
// 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);
}
}
// Remove bridges from top surface polygons.
top = diff_ex(top, current_slices_bridges);
}
bool uncovered = false;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
// 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));
if (uncovered) {
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
return intersection_ex(top, contour);
};
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);
}
// 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;
};
// 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.
// 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();
perimeters = std::move(full_perimeters);
infill_contour = union_ex(full_inner_contour);
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);
}
}
}
//PS
loop_number = int(perimeters.size()) - 1;
bool uncovered = false;
top_expolygons = get_top_expolygons(infill_contour, uncovered);
#ifdef ARACHNE_DEBUG
{
static int iRun = 0;
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
if (uncovered) {
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());
} else {
// There is no top surface ExPolygon, so use the walls generated like when the single perimeter
// feature is disabled.
generate_full_perimeters();
perimeters = std::move(full_perimeters);
infill_contour = union_ex(full_inner_contour);
}
#endif
}
//PS
// 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;
}());
loop_number = int(perimeters.size()) - 1;
int start_perimeter = int(perimeters.size()) - 1;
int end_perimeter = -1;
int direction = -1;
#ifdef ARACHNE_DEBUG
{
static int iRun = 0;
export_perimeters_to_svg(debug_out_path("arachne-perimeters-%d-%d.svg", layer_id, iRun++), to_polygons(last), perimeters, union_ex(wallToolPaths.getInnerContour()));
}
#endif
bool is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner ||
this->config->wall_sequence == WallSequence::InnerOuterInner;
// All closed ExtrusionLine should have the same the first and the last point.
// But in rare cases, Arachne produce ExtrusionLine marked as closed but without
// equal the first and the last point.
assert([&perimeters = std::as_const(perimeters)]() -> bool {
for (const Arachne::VariableWidthLines& perimeter : perimeters)
for (const Arachne::ExtrusionLine& el : perimeter)
if (el.is_closed && el.junctions.front().p != el.junctions.back().p)
return false;
return true;
}());
int start_perimeter = int(perimeters.size()) - 1;
int end_perimeter = -1;
int direction = -1;
bool is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner ||
this->config->wall_sequence == WallSequence::InnerOuterInner;
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
}
if (is_outer_wall_first) {
start_perimeter = 0;
end_perimeter = int(perimeters.size());
direction = 1;
if (layer_id == 0){ // disable inner outer inner algorithm after the first layer
is_outer_wall_first =
this->config->wall_sequence == WallSequence::OuterInner || this->outer_walls_first_for_brim();
}
if (is_outer_wall_first) {
start_perimeter = 0;
end_perimeter = int(perimeters.size());
direction = 1;
}
std::vector<Arachne::ExtrusionLine*> all_extrusions;
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
if (perimeters[perimeter_idx].empty())
continue;
for (Arachne::ExtrusionLine& wall : perimeters[perimeter_idx])
all_extrusions.emplace_back(&wall);
}
// Find topological order with constraints from extrusions_constrains.
std::vector<size_t> blocked(all_extrusions.size(), 0); // Value indicating how many extrusions it is blocking (preceding extrusions) an extrusion.
std::vector<std::vector<size_t>> blocking(all_extrusions.size()); // Each extrusion contains a vector of extrusions that are blocked by this extrusion.
std::unordered_map<const Arachne::ExtrusionLine*, size_t> map_extrusion_to_idx;
for (size_t idx = 0; idx < all_extrusions.size(); idx++)
map_extrusion_to_idx.emplace(all_extrusions[idx], idx);
auto extrusions_constrains = Arachne::WallToolPaths::getRegionOrder(all_extrusions, is_outer_wall_first);
for (auto [before, after] : extrusions_constrains) {
auto after_it = map_extrusion_to_idx.find(after);
++blocked[after_it->second];
blocking[map_extrusion_to_idx.find(before)->second].emplace_back(after_it->second);
}
std::vector<bool> processed(all_extrusions.size(), false); // Indicate that the extrusion was already processed.
Point current_position = all_extrusions.empty() ? Point::Zero() : all_extrusions.front()->junctions.front().p; // Some starting position.
std::vector<PerimeterGeneratorArachneExtrusion> ordered_extrusions; // To store our result in. At the end we'll std::swap.
ordered_extrusions.reserve(all_extrusions.size());
while (ordered_extrusions.size() < all_extrusions.size()) {
size_t best_candidate = 0;
double best_distance_sqr = std::numeric_limits<double>::max();
bool is_best_closed = false;
std::vector<size_t> available_candidates;
for (size_t candidate = 0; candidate < all_extrusions.size(); ++candidate) {
if (processed[candidate] || blocked[candidate])
continue; // Not a valid candidate.
available_candidates.push_back(candidate);
}
std::vector<Arachne::ExtrusionLine*> all_extrusions;
for (int perimeter_idx = start_perimeter; perimeter_idx != end_perimeter; perimeter_idx += direction) {
if (perimeters[perimeter_idx].empty())
std::sort(available_candidates.begin(), available_candidates.end(), [&all_extrusions](const size_t a_idx, const size_t b_idx) -> bool {
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
});
for (const size_t candidate_path_idx : available_candidates) {
auto& path = all_extrusions[candidate_path_idx];
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
if (best_distance_sqr == std::numeric_limits<double>::max()) {
best_candidate = candidate_path_idx;
is_best_closed = path->is_closed;
}
continue;
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 {
return all_extrusions[a_idx]->is_closed < all_extrusions[b_idx]->is_closed;
});
for (const size_t candidate_path_idx : available_candidates) {
auto& path = all_extrusions[candidate_path_idx];
if (path->junctions.empty()) { // No vertices in the path. Can't find the start position then or really plan it in. Put that at the end.
if (best_distance_sqr == std::numeric_limits<double>::max()) {
best_candidate = candidate_path_idx;
is_best_closed = path->is_closed;
}
continue;
}
const Point candidate_position = path->junctions.front().p;
double distance_sqr = (current_position - candidate_position).cast<double>().norm();
if (distance_sqr < best_distance_sqr) { // Closer than the best candidate so far.
if (path->is_closed || (!path->is_closed && best_distance_sqr != std::numeric_limits<double>::max()) || (!path->is_closed && !is_best_closed)) {
best_candidate = candidate_path_idx;
best_distance_sqr = distance_sqr;
is_best_closed = path->is_closed;
}
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.
}
}
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
// To address any remaining scenarios where the outer perimeter contour is not first on the list as arachne sometimes reorders the perimeters when clustering
// for OI mode that is used the basis for IOI
bringContoursToFront(ordered_extrusions);
std::vector<PerimeterGeneratorArachneExtrusion> reordered_extrusions;
// Debug statement to print spacing values:
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
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]--;
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
coord_t threshold_external = (apply_precise_outer_wall)
// Precise outer wall: use the full external spacing
? ( this->ext_perimeter_flow.scaled_spacing()
+ this->perimeter_flow.scaled_spacing()/2.0 )
// Normal: half ext spacing plus half int spacing
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
+ this->perimeter_flow.scaled_spacing()/2.0 );
if (!best_path->junctions.empty()) { //If all paths were empty, the best path is still empty. We don't upate the current position then.
if (best_path->is_closed)
current_position = best_path->junctions[0].p; //We end where we started.
else
current_position = best_path->junctions.back().p; //Pick the other end from where we started.
}
}
// printf("New Layer: Layer ID %d\n",layer_id); //debug - new layer
if (this->config->wall_sequence == WallSequence::InnerOuterInner && layer_id > 0) { // only enable inner outer inner algorithm after first layer
if (ordered_extrusions.size() > 2) { // 3 walls minimum needed to do inner outer inner ordering
int position = 0; // index to run the re-ordering for multiple external perimeters in a single island.
int arr_i, arr_j = 0; // indexes to run through the walls in the for loops
int outer, first_internal, second_internal, max_internal, current_perimeter; // allocate index values
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
// 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;
// Re-order extrusions based on distance
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
// Debug statement to print spacing values:
//printf("External threshold - Ext perimeter: %d Ext spacing: %d Int perimeter: %d Int spacing: %d\n", this->ext_perimeter_flow.scaled_width(),this->ext_perimeter_flow.scaled_spacing(),this->perimeter_flow.scaled_width(), this->perimeter_flow.scaled_spacing());
// Get searching thresholds. For an external perimeter we take the external perimeter spacing/2 plus the internal perimeter spacing/2 and expand by the factor
// rounding errors. When precise wall is enabled, the external perimeter full spacing is used.
coord_t threshold_external = (apply_precise_outer_wall)
// Precise outer wall ⇒ use “full external spacing”
? ( this->ext_perimeter_flow.scaled_spacing()
+ this->perimeter_flow.scaled_spacing()/2.0 )
// Normal ⇒ half ext spacing + half int spacing
: ( this->ext_perimeter_flow.scaled_spacing()/2.0
+ this->perimeter_flow.scaled_spacing()/2.0 );
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
// We then advance the position index to move to the second island and continue until there are no more
// perimeters left.
while (position < reordered_extrusions.size()) {
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
// run through the walls to get the index values that need re-ordering until the first one for each
// is found. Start at "position" index to enable the for loop to iterate for multiple external
// perimeters in a single island
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
case 0: // external perimeter
if (outer == -1)
outer = arr_i;
break;
case 1: // first internal wall
if (first_internal==-1 && arr_i>outer && outer!=-1){
first_internal = arr_i;
}
break;
case 2: // second internal wall
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
second_internal = arr_i;
}
break;
}
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
// This means we entered a new island.
arr_i=arr_i-1; //step back one perimeter
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
break; // exit the for loop
}
}
// printf("Layer ID %d, Outer index %d, inner index %d, second inner index %d, maximum internal perimeter %d \n",layer_id,outer,first_internal,second_internal, max_internal);
if (outer > -1 && first_internal > -1 && second_internal > -1) { // found all three perimeters to re-order? If not the perimeters will be processed outside in.
std::vector<PerimeterGeneratorArachneExtrusion> inner_outer_extrusions; // temporary array to hold extrusions for reordering
inner_outer_extrusions.resize(max_internal - position + 1); // reserve array containing the number of perimeters before a new island. Variables are array indexes hence need to add +1 to convert to position allocations
// printf("Allocated array size %d, max_internal index %d, start position index %d \n",max_internal-position+1,max_internal,position);
for (arr_j = max_internal; arr_j >=position; --arr_j){ // go inside out towards the external perimeter (perimeters in reverse order) and store all internal perimeters until the first one identified with inset index 2
if(arr_j >= second_internal){
//printf("Inside out loop: Mapped perimeter index %d to array position %d\n", arr_j, max_internal-arr_j);
inner_outer_extrusions[max_internal-arr_j] = reordered_extrusions[arr_j];
current_perimeter++;
// For the intenal perimeter threshold, the distance is the internal perimeter spacing expanded by the factor to cover rounding errors.
coord_t threshold_internal = this->perimeter_flow.scaled_spacing();
// Re-order extrusions based on distance
// Alorithm will aggresively optimise for the appearance of the outermost perimeter
ordered_extrusions = reorderPerimetersByProximity(ordered_extrusions,threshold_external,threshold_internal );
reordered_extrusions = ordered_extrusions; // copy them into the reordered extrusions vector to allow for IOI operations to be performed below without altering the base ordered extrusions list.
// Now start the sandwich mode wall re-ordering using the reordered_extrusions as the basis
// scan to find the external perimeter, first internal, second internal and last perimeter in the island.
// We then advance the position index to move to the second island and continue until there are no more
// perimeters left.
while (position < reordered_extrusions.size()) {
outer = first_internal = second_internal = current_perimeter = -1; // initialise all index values to -1
max_internal = reordered_extrusions.size()-1; // initialise the maximum internal perimeter to the last perimeter on the extrusion list
// run through the walls to get the index values that need re-ordering until the first one for each
// is found. Start at "position" index to enable the for loop to iterate for multiple external
// perimeters in a single island
// printf("Reorder Loop. Position %d, extrusion list size: %d, Outer index %d, inner index %d, second inner index %d\n", position, reordered_extrusions.size(),outer,first_internal,second_internal);
for (arr_i = position; arr_i < reordered_extrusions.size(); ++arr_i) {
// printf("Perimeter: extrusion inset index %d, ordered extrusions array position %d\n",reordered_extrusions[arr_i].extrusion->inset_idx, arr_i);
switch (reordered_extrusions[arr_i].extrusion->inset_idx) {
case 0: // external perimeter
if (outer == -1)
outer = arr_i;
break;
case 1: // first internal wall
if (first_internal==-1 && arr_i>outer && outer!=-1){
first_internal = arr_i;
}
}
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];
break;
case 2: // second internal wall
if (second_internal == -1 && arr_i > first_internal && outer!=-1){
second_internal = arr_i;
}
break;
}
if(outer >-1 && first_internal>-1 && reordered_extrusions[arr_i].extrusion->inset_idx == 0){ // found a new external perimeter after we've found at least a first internal perimeter to re-order.
// This means we entered a new island.
arr_i=arr_i-1; //step back one perimeter
max_internal = arr_i; // new maximum internal perimeter is now this as we have found a new external perimeter, hence a new island.
break; // exit the for loop
}
// go to the next perimeter from the current position to continue scanning for external walls in the same island
position = arr_i + 1;
}
// 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++;
}
}
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
position = arr_i + 1;
}
}
}
bool steep_overhang_contour = false;
bool steep_overhang_hole = false;
if (!config->overhang_reverse) {
// Skip steep overhang detection no reverse is specified
steep_overhang_contour = true;
steep_overhang_hole = true;
}
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
if (config->overhang_reverse) {
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
}
defer_unsupported_loops(*this, extrusion_coll);
result.loops = std::move(extrusion_coll);
result.has_loops = true;
bool steep_overhang_contour = false;
bool steep_overhang_hole = false;
if (!config->overhang_reverse) {
// Skip steep overhang detection no reverse is specified
steep_overhang_contour = true;
steep_overhang_hole = true;
}
if (ExtrusionEntityCollection extrusion_coll = traverse_extrusions(*this, ordered_extrusions, steep_overhang_contour, steep_overhang_hole); !extrusion_coll.empty()) {
if (config->overhang_reverse) {
reorient_perimeters(extrusion_coll, steep_overhang_contour, steep_overhang_hole,
this->config->overhang_reverse_internal_only);
}
defer_unsupported_loops(*this, extrusion_coll);
this->loops->append(extrusion_coll);
}
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
const coord_t spacing = (perimeters.size() == 1) ? ext_perimeter_spacing2 : perimeter_spacing;
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out.
if (offset_ex(infill_contour, -float(spacing / 2.)).empty())
infill_contour.clear(); // Infill region is too small, so let's filter it out.
// create one more offset to be used as boundary for fill
// we offset by half the perimeter spacing (to get to the actual infill boundary)
// and then we offset back and forth by half the infill spacing to only consider the
// non-collapsing regions
coord_t inset =
(loop_number < 0) ? 0 :
(loop_number == 0) ?
// one loop
ext_perimeter_spacing :
// two or more loops?
perimeter_spacing;
coord_t top_inset = inset;
// create one more offset to be used as boundary for fill
// we offset by half the perimeter spacing (to get to the actual infill boundary)
// and then we offset back and forth by half the infill spacing to only consider the
// non-collapsing regions
coord_t inset =
(loop_number < 0) ? 0 :
(loop_number == 0) ?
// one loop
ext_perimeter_spacing :
// two or more loops?
perimeter_spacing;
coord_t top_inset = inset;
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
if(is_topmost_layer || is_bottom_layer)
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
else
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
top_inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
if(is_topmost_layer || is_bottom_layer)
inset = coord_t(scale_(this->config->top_bottom_infill_wall_overlap.get_abs_value(unscale<double>(inset))));
else
inset = coord_t(scale_(this->config->infill_wall_overlap.get_abs_value(unscale<double>(inset))));
// simplify infill contours according to resolution
Polygons pp;
for (ExPolygon& ex : infill_contour)
ex.simplify_p(m_scaled_resolution, &pp);
ExPolygons not_filled_exp = union_ex(pp);
// collapse too narrow infill areas
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
// simplify infill contours according to resolution
Polygons pp;
for (ExPolygon& ex : infill_contour)
ex.simplify_p(m_scaled_resolution, &pp);
ExPolygons not_filled_exp = union_ex(pp);
// collapse too narrow infill areas
const auto min_perimeter_infill_spacing = coord_t(solid_infill_spacing * (1. - INSET_OVERLAP_TOLERANCE));
ExPolygons infill_exp = offset2_ex(
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,
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)));
}
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));
float(+min_perimeter_infill_spacing / 2.));
if (!top_expolygons.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_expolygons);
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
}
}
}
+2 -2
View File
@@ -34,7 +34,7 @@ public:
explicit Polygon(const Points &points) : MultiPoint(points) {}
Polygon(std::initializer_list<Point> points) : MultiPoint(points) {}
Polygon(const Polygon &other) : MultiPoint(other.points) {}
Polygon(Polygon &&other) noexcept : MultiPoint(std::move(other.points)) {}
Polygon(Polygon &&other) : MultiPoint(std::move(other.points)) {}
static Polygon new_scale(const std::vector<Vec2d> &points) {
Polygon pgn;
pgn.points.reserve(points.size());
@@ -43,7 +43,7 @@ public:
return pgn;
}
Polygon& operator=(const Polygon &other) { points = other.points; return *this; }
Polygon& operator=(Polygon &&other) noexcept { points = std::move(other.points); return *this; }
Polygon& operator=(Polygon &&other) { points = std::move(other.points); return *this; }
Point& operator[](Points::size_type idx) { return this->points[idx]; }
const Point& operator[](Points::size_type idx) const { return this->points[idx]; }
+2 -2
View File
@@ -26,7 +26,7 @@ class Polyline : public MultiPoint {
public:
Polyline() {};
Polyline(const Polyline& other) : MultiPoint(other.points), fitting_result(other.fitting_result) {}
Polyline(Polyline &&other) noexcept : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
Polyline(Polyline &&other) : MultiPoint(std::move(other.points)), fitting_result(std::move(other.fitting_result)) {}
Polyline(std::initializer_list<Point> list) : MultiPoint(list) {
fitting_result.clear();
}
@@ -47,7 +47,7 @@ public:
fitting_result = other.fitting_result;
return *this;
}
Polyline& operator=(Polyline&& other) noexcept {
Polyline& operator=(Polyline&& other) {
points = std::move(other.points);
fitting_result = std::move(other.fitting_result);
return *this;
+3 -3
View File
@@ -1183,9 +1183,9 @@ static std::vector<std::string> s_Preset_print_options{
"is_infill_first",
"sparse_infill_density",
"fill_multiline",
"adaptive_infill_mode",
"adaptive_infill_interior_density",
"adaptive_infill_gradient",
"tpms_adaptive",
"tpms_interior_density",
"tpms_adaptive_gradient",
"gyroid_optimized",
"sparse_infill_pattern",
"sparse_infill_smooth_factor",
+4 -4
View File
@@ -12,7 +12,7 @@
#include "PrintBase.hpp"
#include "Fill/FillAdaptive.hpp"
#include "Fill/FillLightning.hpp"
#include "Fill/AdaptiveInfillField.hpp"
#include "Fill/FillTpmsAdaptive.hpp"
#include "BoundingBox.hpp"
#include "ExtrusionEntityCollection.hpp"
@@ -407,7 +407,7 @@ public:
double max_z() const { return m_max_z; }
// Centering offset of the sliced mesh from the scaled and rotated mesh of the model.
const Point& center_offset() const { return m_center_offset; }
const AdaptiveInfillField* adaptive_infill_field(AdaptiveInfillMode mode) const { return m_adaptive_infill_fields[size_t(mode)].get(); }
const TpmsRadialField* tpms_radial_field(TpmsAdaptiveMode mode) const { return m_tpms_radial_fields[size_t(mode)].get(); }
// BBS
void generate_support_preview();
@@ -655,7 +655,7 @@ private:
FillAdaptive::RegionOctrees prepare_adaptive_infill_data(
const std::vector<std::pair<const Surface*, const Layer*>>& surfaces_w_layer) const;
FillLightning::GeneratorPtr prepare_lightning_infill_data();
AdaptiveInfillFields prepare_adaptive_infill_fields() const;
TpmsRadialFields prepare_tpms_radial_fields() const;
// BBS
SupportNecessaryType is_support_necessary();
@@ -706,7 +706,7 @@ private:
FillAdaptive::RegionOctrees m_adaptive_fill_octrees;
std::vector<BoundingBox> m_separated_body_bboxes;
FillLightning::GeneratorPtr m_lightning_generator;
AdaptiveInfillFields m_adaptive_infill_fields;
TpmsRadialFields m_tpms_radial_fields;
std::vector < VolumeSlices > firstLayerObjSliceByVolume;
std::vector<groupedVolumeSlices> firstLayerObjSliceByGroups;
+22 -28
View File
@@ -366,24 +366,24 @@ static t_config_enum_values s_keys_map_SurfaceFillOrder{
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(SurfaceFillOrder)
//Orca
static t_config_enum_values s_keys_map_AdaptiveInfillMode{
{ "disabled", int(AdaptiveInfillMode::Disabled) },
{ "distance_warp", int(AdaptiveInfillMode::DistanceWarp) },
{ "smooth_blend", int(AdaptiveInfillMode::SmoothBlend) },
{ "stepped_shells", int(AdaptiveInfillMode::SteppedShells) },
{ "lobes", int(AdaptiveInfillMode::Lobes) },
{ "normal_z", int(AdaptiveInfillMode::NormalZ) },
{ "normal_y", int(AdaptiveInfillMode::NormalY) },
{ "normal_x", int(AdaptiveInfillMode::NormalX) },
static t_config_enum_values s_keys_map_TpmsAdaptiveMode{
{ "disabled", int(TpmsAdaptiveMode::Disabled) },
{ "distance_warp", int(TpmsAdaptiveMode::DistanceWarp) },
{ "smooth_blend", int(TpmsAdaptiveMode::SmoothBlend) },
{ "stepped_shells", int(TpmsAdaptiveMode::SteppedShells) },
{ "lobes", int(TpmsAdaptiveMode::Lobes) },
{ "normal_z", int(TpmsAdaptiveMode::NormalZ) },
{ "normal_y", int(TpmsAdaptiveMode::NormalY) },
{ "normal_x", int(TpmsAdaptiveMode::NormalX) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillMode)
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveMode)
static t_config_enum_values s_keys_map_AdaptiveInfillGradient{
{ "linear", int(AdaptiveInfillGradient::Linear) },
{ "quadratic", int(AdaptiveInfillGradient::Quadratic) },
{ "exponential", int(AdaptiveInfillGradient::Exponential) },
static t_config_enum_values s_keys_map_TpmsAdaptiveGradient{
{ "linear", int(TpmsAdaptiveGradient::Linear) },
{ "quadratic", int(TpmsAdaptiveGradient::Quadratic) },
{ "exponential", int(TpmsAdaptiveGradient::Exponential) },
};
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(AdaptiveInfillGradient)
CONFIG_OPTION_ENUM_DEFINE_STATIC_MAPS(TpmsAdaptiveGradient)
//BBS
static t_config_enum_values s_keys_map_PrintSequence {
@@ -3660,7 +3660,7 @@ void PrintConfigDef::init_fff_params()
def->max = 10; // Maximum number of lines for infill pattern
def->set_default_value(new ConfigOptionInt(1));
def = this->add("adaptive_infill_mode", coEnum);
def = this->add("tpms_adaptive", coEnum);
def->label = L("Adaptive density (experimental)");
def->category = L("Strength");
def->tooltip = L("Grades the Gyroid and TPMS infill inside the object: its cells grow from the surface of the "
@@ -3678,7 +3678,7 @@ void PrintConfigDef::init_fff_params()
"joined to the rest by a narrower neck, is graded towards its own center.\n"
" - Normal Z, Y or X: follows the sections of the object normal to that axis, so the density "
"does not change along it.");
def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillMode>::get_enum_values();
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveMode>::get_enum_values();
def->enum_values.push_back("disabled");
def->enum_values.push_back("distance_warp");
def->enum_values.push_back("smooth_blend");
@@ -3696,9 +3696,9 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Normal Y"));
def->enum_labels.push_back(L("Normal X"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<AdaptiveInfillMode>(AdaptiveInfillMode::Disabled));
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveMode>(TpmsAdaptiveMode::Disabled));
def = this->add("adaptive_infill_interior_density", coPercent);
def = this->add("tpms_interior_density", coPercent);
def->label = L("Interior density");
def->category = L("Strength");
def->tooltip = L("Density of the adaptive infill at the center of the object.");
@@ -3708,7 +3708,7 @@ void PrintConfigDef::init_fff_params()
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionPercent(5));
def = this->add("adaptive_infill_gradient", coEnum);
def = this->add("tpms_adaptive_gradient", coEnum);
def->label = L("Adaptive gradient");
def->category = L("Strength");
def->tooltip = L("How the density changes from the surface to the center of the object.\n"
@@ -3717,7 +3717,7 @@ void PrintConfigDef::init_fff_params()
"changes faster towards the center.\n"
"Exponential: the density changes quickly just below the surface and levels off towards "
"the center.");
def->enum_keys_map = &ConfigOptionEnum<AdaptiveInfillGradient>::get_enum_values();
def->enum_keys_map = &ConfigOptionEnum<TpmsAdaptiveGradient>::get_enum_values();
def->enum_values.push_back("linear");
def->enum_values.push_back("quadratic");
def->enum_values.push_back("exponential");
@@ -3725,7 +3725,7 @@ void PrintConfigDef::init_fff_params()
def->enum_labels.push_back(L("Quadratic"));
def->enum_labels.push_back(L("Exponential"));
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionEnum<AdaptiveInfillGradient>(AdaptiveInfillGradient::Linear));
def->set_default_value(new ConfigOptionEnum<TpmsAdaptiveGradient>(TpmsAdaptiveGradient::Linear));
// Z-buckling bias optimization (experimental). Tightens the gyroid wave along the Z
// (vertical) axis at low infill density to shorten the effective column length under
@@ -9665,12 +9665,6 @@ void PrintConfigDef::handle_legacy(t_config_option_key &opt_key, std::string &va
opt_key = "change_filament_gcode";
} else if (opt_key == "bridge_fan_speed") {
opt_key = "overhang_fan_speed";
} else if (opt_key == "tpms_adaptive") {
opt_key = "adaptive_infill_mode";
} else if (opt_key == "tpms_interior_density") {
opt_key = "adaptive_infill_interior_density";
} else if (opt_key == "tpms_adaptive_gradient") {
opt_key = "adaptive_infill_gradient";
} else if (opt_key == "infill_extruder" || opt_key == "sparse_infill_filament") {
// ORCA: legacy feature-filament selector. Pre-2.4.0-dev these keys were 1-based and the
// default value "1" meant "the first/active filament". The current scheme uses a dedicated
+11 -11
View File
@@ -172,8 +172,8 @@ inline bool is_separable_infill_pattern(InfillPattern pattern)
// Orca: Infill patterns laid out by an octree, which each connected body always gets of its own.
inline bool is_octree_infill_pattern(InfillPattern pattern) { return pattern == ipAdaptiveCubic || pattern == ipSupportCubic; }
// Orca: Infill patterns graded by the "adaptive_infill_mode" option.
inline bool is_adaptive_infill_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
// Orca: Infill patterns graded by the "tpms_adaptive" option.
inline bool is_tpms_adaptive_pattern(InfillPattern pattern) { return pattern == ipGyroid || pattern == ipTpmsD || pattern == ipTpmsFK; }
// Orca: Infill patterns that round their corners by the "sparse_infill_smooth_factor" option.
// Grid, Triangles and Tri-hexagon only do so in their trapezoidal form, which is generated with more
@@ -279,8 +279,8 @@ enum class SurfaceFillOrder {
Count,
};
// Orca: what the adaptive infill density follows: the 3D shape of the object, or its 2D sections normal to an axis.
enum class AdaptiveInfillMode {
// Orca: what the adaptive TPMS density follows: the 3D shape of the object, or its 2D sections normal to an axis.
enum class TpmsAdaptiveMode {
Disabled,
DistanceWarp,
SmoothBlend,
@@ -292,8 +292,8 @@ enum class AdaptiveInfillMode {
Count,
};
// Orca: how the adaptive infill density changes from the object surface to its deepest point.
enum class AdaptiveInfillGradient {
// Orca: how the adaptive TPMS density changes from the object surface to its deepest point.
enum class TpmsAdaptiveGradient {
Linear,
Quadratic,
Exponential,
@@ -816,8 +816,8 @@ CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PerimeterGeneratorType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(ToolChangeOrderingType)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(PowerLossRecoveryMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(SurfaceFillOrder)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(AdaptiveInfillGradient)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveMode)
CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS(TpmsAdaptiveGradient)
#undef CONFIG_OPTION_ENUM_DECLARE_STATIC_MAPS
@@ -1501,9 +1501,9 @@ PRINT_CONFIG_CLASS_DEFINE(
// Orca:
((ConfigOptionFloatOrPercent, infill_combination_max_layer_height))
((ConfigOptionInt, fill_multiline))
((ConfigOptionEnum<AdaptiveInfillMode>, adaptive_infill_mode))
((ConfigOptionPercent, adaptive_infill_interior_density))
((ConfigOptionEnum<AdaptiveInfillGradient>, adaptive_infill_gradient))
((ConfigOptionEnum<TpmsAdaptiveMode>, tpms_adaptive))
((ConfigOptionPercent, tpms_interior_density))
((ConfigOptionEnum<TpmsAdaptiveGradient>, tpms_adaptive_gradient))
((ConfigOptionBool, gyroid_optimized))
// Ironing options
((ConfigOptionEnum<IroningType>, ironing_type))
+197 -365
View File
@@ -33,7 +33,7 @@
#include "Fill/FillAdaptive.hpp"
#include "Fill/Fill.hpp"
#include "Fill/FillLightning.hpp"
#include "Fill/AdaptiveInfillField.hpp"
#include "Fill/FillTpmsAdaptive.hpp"
#include "format.hpp"
#include "AABBTreeIndirect.hpp"
#include "AABBTreeLines.hpp"
@@ -50,7 +50,6 @@
#include <cstdlib>
#include <cstdint>
#include <float.h>
#include <array>
#include <functional>
#include <ios>
#include <iomanip>
@@ -77,7 +76,6 @@
#include <Eigen/Core>
#include <tbb/parallel_for.h>
#include <tbb/parallel_invoke.h>
#include <tbb/spin_mutex.h>
#include <tbb/concurrent_unordered_set.h>
@@ -1254,19 +1252,19 @@ FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data()
return has_lightning_infill ? FillLightning::build_generator(std::as_const(*this), [this]() -> void { this->throw_if_canceled(); }) : FillLightning::GeneratorPtr();
}
AdaptiveInfillFields PrintObject::prepare_adaptive_infill_fields() const
TpmsRadialFields PrintObject::prepare_tpms_radial_fields() const
{
AdaptiveInfillFields fields;
std::array<bool, size_t(AdaptiveInfillMode::Count)> modes{};
TpmsRadialFields fields;
std::array<bool, size_t(TpmsAdaptiveMode::Count)> modes{};
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id)
if (const PrintRegionConfig &config = this->printing_region(region_id).config();
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_adaptive_infill_pattern(config.sparse_infill_pattern))
modes[size_t(config.adaptive_infill_mode.value)] = true;
modes[size_t(AdaptiveInfillMode::Disabled)] = false;
config.sparse_infill_density > 0 && config.sparse_infill_density < 100 && is_tpms_adaptive_pattern(config.sparse_infill_pattern))
modes[size_t(config.tpms_adaptive.value)] = true;
modes[size_t(TpmsAdaptiveMode::Disabled)] = false;
if (std::find(modes.begin(), modes.end(), true) == modes.end() || m_layers.empty())
return fields;
std::vector<AdaptiveInfillField::Slice> slices;
std::vector<TpmsRadialField::Slice> slices;
slices.reserve(m_layers.size());
BoundingBox bbox;
for (const Layer *layer : m_layers) {
@@ -1279,7 +1277,7 @@ AdaptiveInfillFields PrintObject::prepare_adaptive_infill_fields() const
if (!modes[mode])
continue;
// Without a field, the infill falls back to the regular pattern.
auto field = std::make_unique<AdaptiveInfillField>(slices, bbox, AdaptiveInfillMode(mode), [this]() { m_print->throw_if_canceled(); });
auto field = std::make_unique<TpmsRadialField>(slices, bbox, TpmsAdaptiveMode(mode), [this]() { m_print->throw_if_canceled(); });
if (!field->empty())
fields[mode] = std::move(field);
}
@@ -1687,9 +1685,9 @@ bool PrintObject::invalidate_state_by_config_options(
|| opt_key == "infill_overhang_angle") {
steps.emplace_back(posInfill);
} else if (opt_key == "sparse_infill_pattern"
|| opt_key == "adaptive_infill_mode"
|| opt_key == "adaptive_infill_interior_density"
|| opt_key == "adaptive_infill_gradient"
|| opt_key == "tpms_adaptive"
|| opt_key == "tpms_interior_density"
|| opt_key == "tpms_adaptive_gradient"
// Orca: Body centering now also determines bridge anchors during preparation.
// Invalidating preparation also invalidates infill, including top/bottom surfaces.
|| opt_key == "center_of_surface_pattern"
@@ -1932,9 +1930,7 @@ void PrintObject::detect_surfaces_type()
bool interface_shells = ! spiral_mode && m_config.interface_shells.value;
size_t num_layers = spiral_mode ? std::min(size_t(this->printing_region(0).config().bottom_shell_layers), m_layers.size()) : m_layers.size();
// 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) {
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " in parallel - start";
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
for (Layer *layer : m_layers)
@@ -1992,7 +1988,7 @@ void PrintObject::detect_surfaces_type()
if (upper_layer) {
ExPolygons upper_slices = interface_shells ?
diff_ex(layerm_slices_surfaces, upper_layer->m_regions[region_id]->slices.surfaces, ApplySafetyOffset::Yes) :
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(upper_layer->lslices), ApplySafetyOffset::Yes);
diff_ex(layerm_slices_surfaces, upper_layer->lslices, ApplySafetyOffset::Yes);
surfaces_append(top, opening_ex(upper_slices, offset), stTop);
} else {
// if no upper layer, all surfaces of this one are solid
@@ -2018,7 +2014,7 @@ void PrintObject::detect_surfaces_type()
surfaces_append(
bottom,
opening_ex(
diff_ex_by_piece(layerm_slices_surfaces, to_polygons(lower_layer->lslices), ApplySafetyOffset::Yes),
diff_ex(layerm_slices_surfaces, lower_layer->lslices, ApplySafetyOffset::Yes),
offset),
surface_type_bottom_other);
// if user requested internal shells, we need to identify surfaces
@@ -2049,44 +2045,34 @@ void PrintObject::detect_surfaces_type()
// and top surfaces; let's do an intersection to discover them and consider them
// as bottom surfaces (to allow for bridge detection)
if (! top.empty() && ! bottom.empty()) {
const auto cracks = intersection_ex_by_piece(to_expolygons(top), to_polygons(bottom));
const auto cracks = intersection_ex(top, bottom);
if (!cracks.empty()) {
if (lower_layer) { // Only detect small cracks for non-first layer, because first layer should always be bottom
const float small_crack_threshold = -layerm->flow(frExternalPerimeter).scaled_width() * 1.5;
// Only the bottom surfaces near a crack can take part: one that contains it must contain its box,
// and one whose box misses the grown crack is left unchanged by removing it. A layer cut through
// a fine relief has thousands of both, which made this loop quadratic.
for (const auto& crack : cracks) {
if (offset_ex(crack, small_crack_threshold).empty()) {
// For small cracks, if it's part of a large bottom surface, then it should be added to bottom as well
const BoundingBox crack_bbox = get_extents(crack);
if (std::any_of(bottom.begin(), bottom.end(), [&crack, &crack_bbox, small_crack_threshold](const Surface& s) {
if (std::any_of(bottom.begin(), bottom.end(), [&crack, small_crack_threshold](const Surface& s) {
const auto& se = s.expolygon;
return get_extents(se).inflated(SCALED_EPSILON).contains(crack_bbox)
&& diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
return diff_ex(crack, se, ApplySafetyOffset::Yes).empty()
&& se.area() > crack.area() * 2
&& !offset_ex(diff_ex(se, crack), small_crack_threshold).empty();
})) continue;
// Crack too small, leave it as part of the top surface, remove it from bottom surfaces
const ExPolygons grown_crack = offset_ex(crack, -small_crack_threshold);
const BoundingBox grown_bbox = get_extents(grown_crack);
Surfaces bot_tmp;
for (auto& b : bottom) {
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));
surfaces_append(bot_tmp, diff_ex(b.expolygon, offset_ex(crack, -small_crack_threshold)), b.surface_type);
}
bottom = std::move(bot_tmp);
}
}
}
ExPolygons top_expolygons = to_expolygons(std::move(top));
Polygons top_polygons = to_polygons(std::move(top));
top.clear();
surfaces_append(top, diff_ex_by_piece(top_expolygons, to_polygons(bottom)), stTop);
surfaces_append(top, diff_ex(top_polygons, bottom), stTop);
}
}
@@ -2177,7 +2163,7 @@ void PrintObject::detect_surfaces_type()
{
Polygons topbottom = to_polygons(top);
polygons_append(topbottom, to_polygons(bottom));
surfaces_append(surfaces_out, diff_ex_by_piece(surfaces_prev_expolys, topbottom), stInternal);
surfaces_append(surfaces_out, diff_ex(surfaces_prev_expolys, topbottom), stInternal);
}
surfaces_append(surfaces_out, std::move(top));
@@ -2354,31 +2340,29 @@ void PrintObject::detect_surfaces_type()
}
}
);
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
tbb::parallel_for( tbb::blocked_range<size_t>(0, m_layers.size()), [this, region_id](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer) {
Surfaces &surfs = m_layers[idx_layer]->m_regions[region_id]->slices.surfaces;
for (Surface &s : surfs) {
if (s.surface_type == stInternalAfterExternalBridge) {
s.surface_type = stBottomBridge;
}
}
}
}
);
}
}
// ==============================================================================================================
// === ORCA: End of second external bridge layer changes =======================================================
// ==============================================================================================================
}); // for each this->print->region_count
// ==============================================================================================================
// === ORCA: Interim workaround - for now the new stInternalAfterExternalBridge surfaace is re-classified ==============
// === back to a bottom bridge. As a starting point, this improves bridging reliability as it extrudes ==========
// === two external bridge layers. However, TODO: Implement a new surface type throughout the codebase ==========
// ==============================================================================================================
// Once all the regions have their second bridge layer, and before their slices are trimmed into fill surfaces below.
if ((this->config().enable_extra_bridge_layer.value == eblApplyToAll) || (this->config().enable_extra_bridge_layer.value == eblExternalBridgeOnly)) {
tbb::parallel_for(tbb::blocked_range<size_t>(0, m_layers.size()), [this](const tbb::blocked_range<size_t> &range) {
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++idx_layer)
for (LayerRegion *layerm : m_layers[idx_layer]->regions())
for (Surface &s : layerm->slices.surfaces)
if (s.surface_type == stInternalAfterExternalBridge)
s.surface_type = stBottomBridge;
});
m_print->throw_if_canceled();
}
tbb::parallel_for(size_t(0), this->num_printing_regions(), [&](size_t region_id) {
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - start";
// Fill in layerm->fill_surfaces by trimming the layerm->slices by the cummulative layerm->fill_surfaces.
tbb::parallel_for(
@@ -2395,7 +2379,7 @@ void PrintObject::detect_surfaces_type()
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Detecting solid surfaces for region " << region_id << " - clipping in parallel - end";
});
} // for each this->print->region_count
// Mark the object to have the region slices classified (typed, which also means they are split based on whether they are supported, bridging, top layers etc.)
m_typed_slices = true;
@@ -2462,10 +2446,8 @@ void PrintObject::process_external_surfaces()
BOOST_LOG_TRIVIAL(debug) << "Collecting surfaces covered with extrusions in parallel - end";
}
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - start";
// The regions of a layer do not see each other here, and a layer cut through a fine relief takes far longer than the
// others, so the regions run next to each other instead of one after another, each still over all layers.
tbb::parallel_for(size_t(0), this->num_printing_regions(), [this, &surfaces_covered](size_t region_id) {
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++region_id) {
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - start";
tbb::parallel_for(
tbb::blocked_range<size_t>(0, m_layers.size()),
[this, &surfaces_covered, region_id](const tbb::blocked_range<size_t>& range) {
@@ -2480,9 +2462,9 @@ void PrintObject::process_external_surfaces()
}
}
);
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces in parallel - end";
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Processing external surfaces for region " << region_id << " in parallel - end";
}
}
void PrintObject::discover_vertical_shells()
@@ -2521,10 +2503,10 @@ void PrintObject::discover_vertical_shells()
// The "ensure vertical wall thickness" feature is not applicable to any of the regions. Quit.
return;
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - start : cache top / bottom";
// One layer per task: on a layer cut through a fine relief the unions below take far longer than elsewhere, and a
// few such layers next to each other must not end up in one task.
//FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1));
tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, 1),
tbb::blocked_range<size_t>(0, num_layers, grain_size),
[this, &cache_top_botom_regions](const tbb::blocked_range<size_t>& range) {
const std::initializer_list<SurfaceType> surfaces_bottom { stBottom, stBottomBridge };
const size_t num_regions = this->num_printing_regions();
@@ -2532,198 +2514,67 @@ void PrintObject::discover_vertical_shells()
m_print->throw_if_canceled();
const Layer &layer = *m_layers[idx_layer];
DiscoverVerticalShellsCacheEntry &cache = cache_top_botom_regions[idx_layer];
const auto top_bottom_expansion = [&layer](size_t region_id) {
return float(layer.m_regions[region_id]->flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
};
// Simulate single set of perimeters over all merged regions.
float perimeter_offset = 0.f;
float perimeter_min_spacing = FLT_MAX;
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
static size_t debug_idx = 0;
++ debug_idx;
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
// The top surfaces, the bottom surfaces and the holes are independent of each other.
tbb::parallel_invoke(
[&]() {
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.top_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_type(stTop), top_bottom_expansion(region_id)));
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Save some computing time by reducing the number of polygons.
cache.top_surfaces = union_(cache.top_surfaces);
},
[&]() {
for (size_t region_id = 0; region_id < num_regions; ++ region_id)
append(cache.bottom_surfaces, offset(layer.m_regions[region_id]->slices.filter_by_types(surfaces_bottom), top_bottom_expansion(region_id)));
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
cache.bottom_surfaces = union_(cache.bottom_surfaces);
},
[&]() {
// Simulate single set of perimeters over all merged regions.
float perimeter_offset = 0.f;
float perimeter_min_spacing = FLT_MAX;
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
const LayerRegion &layerm = *layer.m_regions[region_id];
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
// First find the maxium number of perimeters per region slice.
unsigned int perimeters = 0;
for (const Surface &s : layerm.slices.surfaces)
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
perimeters += layerm.region().config().wall_loops.value;
// Then calculate the infill offset.
if (perimeters > 0) {
Flow extflow = layerm.flow(frExternalPerimeter);
Flow flow = layerm.flow(frPerimeter);
perimeter_offset = std::max(perimeter_offset,
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
}
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
}
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
if (perimeter_offset > 0.) {
// The layer.lslices are forced to merge by expanding them first.
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
for (size_t region_id = 0; region_id < num_regions; ++ region_id) {
LayerRegion &layerm = *layer.m_regions[region_id];
float top_bottom_expansion = float(layerm.flow(frSolidInfill).scaled_spacing()) * top_bottom_expansion_coeff;
// Top surfaces.
append(cache.top_surfaces, offset(layerm.slices.filter_by_type(stTop), top_bottom_expansion));
// append(cache.top_surfaces, offset(layerm.fill_surfaces.filter_by_type(stTop), top_bottom_expansion));
// Bottom surfaces.
append(cache.bottom_surfaces, offset(layerm.slices.filter_by_types(surfaces_bottom), top_bottom_expansion));
// append(cache.bottom_surfaces, offset(layerm.fill_surfaces.filter_by_types(surfaces_bottom), top_bottom_expansion));
// Calculate the maximum perimeter offset as if the slice was extruded with a single extruder only.
// First find the maxium number of perimeters per region slice.
unsigned int perimeters = 0;
for (Surface &s : layerm.slices.surfaces)
perimeters = std::max<unsigned int>(perimeters, s.extra_perimeters);
perimeters += layerm.region().config().wall_loops.value;
// Then calculate the infill offset.
if (perimeters > 0) {
Flow extflow = layerm.flow(frExternalPerimeter);
Flow flow = layerm.flow(frPerimeter);
perimeter_offset = std::max(perimeter_offset,
0.5f * float(extflow.scaled_width() + extflow.scaled_spacing()) + (float(perimeters) - 1.f) * flow.scaled_spacing());
perimeter_min_spacing = std::min(perimeter_min_spacing, float(std::min(extflow.scaled_spacing(), flow.scaled_spacing())));
}
polygons_append(cache.holes, to_polygons(layerm.fill_expolygons));
}
// Save some computing time by reducing the number of polygons.
cache.top_surfaces = union_(cache.top_surfaces);
cache.bottom_surfaces = union_(cache.bottom_surfaces);
// For a multi-material print, simulate perimeter / infill split as if only a single extruder has been used for the whole print.
if (perimeter_offset > 0.) {
// The layer.lslices are forced to merge by expanding them first.
polygons_append(cache.holes, offset2(layer.lslices, 0.3f * perimeter_min_spacing, - perimeter_offset - 0.3f * perimeter_min_spacing));
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
svg.draw(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close();
}
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-extra-holes-%d.svg", debug_idx), get_extents(layer.lslices));
svg.draw(layer.lslices, "blue");
svg.draw(union_ex(cache.holes), "red");
svg.draw_outline(union_ex(cache.holes), "black", "blue", scale_(0.05));
svg.Close();
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
}
cache.holes = union_(cache.holes);
});
}
cache.holes = union_(cache.holes);
}
});
m_print->throw_if_canceled();
BOOST_LOG_TRIVIAL(debug) << "Discovering vertical shells in parallel - end : cache top / bottom";
}
// 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) {
for (size_t region_id = 0; region_id < this->num_printing_regions(); ++ region_id) {
const PrintRegion &region = this->printing_region(region_id);
if (region.config().ensure_vertical_shell_thickness.value != evstAll )
// This region will be handled by discover_horizontal_shells().
return;
continue;
//FIXME Improve the heuristics for a grain size.
size_t grain_size = std::max(num_layers / 16, size_t(1));
@@ -2763,7 +2614,7 @@ void PrintObject::discover_vertical_shells()
grain_size = 1;
tbb::parallel_for(
tbb::blocked_range<size_t>(0, num_layers, grain_size),
[this, region_id, &shell_accumulations, &accumulation_key, &accumulate_shell]
[this, region_id, &cache_top_botom_regions]
(const tbb::blocked_range<size_t>& range) {
// printf("discover_vertical_shells from %d to %d\n", range.begin(), range.end());
for (size_t idx_layer = range.begin(); idx_layer < range.end(); ++ idx_layer) {
@@ -2813,19 +2664,80 @@ void PrintObject::discover_vertical_shells()
}
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
const AccumulationKey key = accumulation_key(region_config, layerm);
const ShellAccumulation *reused = shell_accumulations.empty() ? nullptr :
[&]() -> const ShellAccumulation * {
for (const ShellAccumulation &a : shell_accumulations[idx_layer])
if (a.key == key)
return &a;
return nullptr;
}();
if (reused != nullptr) {
shell = reused->shell;
holes = reused->holes;
} else
accumulate_shell(idx_layer, region_config, layerm, shell, holes);
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);
}
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
Slic3r::SVG svg(debug_out_path("discover_vertical_shells-perimeters-before-union-%d.svg", debug_idx), get_extents(shell));
@@ -2919,8 +2831,11 @@ void PrintObject::discover_vertical_shells()
Polygons object_volume;
Polygons internal_volume;
{
if (idx_layer > 0 && idx_layer + 1 < m_layers.size())
object_volume = to_polygons(intersection_ex_by_piece(m_layers[idx_layer - 1]->lslices, to_polygons(m_layers[idx_layer + 1]->lslices)));
Polygons shrinked_bottom_slice = idx_layer > 0 ? to_polygons(m_layers[idx_layer - 1]->lslices) : Polygons{};
Polygons shrinked_upper_slice = (idx_layer + 1) < m_layers.size() ?
to_polygons(m_layers[idx_layer + 1]->lslices) :
Polygons{};
object_volume = intersection(shrinked_bottom_slice, shrinked_upper_slice);
internal_volume = closing(polygonsInternal, SCALED_EPSILON);
}
@@ -2931,34 +2846,15 @@ void PrintObject::discover_vertical_shells()
// the in-model condition is there due to small sloping surfaces, e.g. top of the hull of the benchy
// 2. the area does not fully cover an internal polygon
// This is there mainly for a very thin parts, where the solid layers would be missing if the part area is quite small
// Both tests below compare a small piece against the whole layer. Done literally, that is
// quadratic in the number of pieces, which is what a layer split up by colour painting has,
// so each is restricted to the part of the layer near the piece with an identical result:
// object_volume is clipped to the piece's box, and only the internal polygons whose box meets
// the expanded piece take part in the count, since the others pass through the difference
// unchanged and add the same number to both sides of it.
std::vector<BoundingBox> internal_bboxes;
internal_bboxes.reserve(internal_volume.size());
for (const Polygon &poly : internal_volume)
internal_bboxes.emplace_back(get_extents(poly));
regularized_shell.erase(std::remove_if(regularized_shell.begin(), regularized_shell.end(),
[&internal_volume, &internal_bboxes, &min_perimeter_infill_spacing,
[&internal_volume, &min_perimeter_infill_spacing,
&object_volume](const ExPolygon &p) {
const bool small = p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p),
ClipperUtils::clip_clipper_polygons_with_subject_bbox(
object_volume, get_extents(p).inflated(SCALED_EPSILON)))
.empty());
if (!small)
return false;
const Polygons expanded = expand(to_polygons(p), min_perimeter_infill_spacing);
const BoundingBox bbox = get_extents(expanded);
Polygons nearby;
for (size_t i = 0; i < internal_volume.size(); ++i)
if (internal_bboxes[i].overlap(bbox))
nearby.emplace_back(internal_volume[i]);
return diff(nearby, expanded).size() >= nearby.size();
return (p.area() < min_perimeter_infill_spacing * scaled(1.5) ||
(p.area() < min_perimeter_infill_spacing * scaled(8.0) &&
diff(to_polygons(p), object_volume).empty())) &&
diff(internal_volume,
expand(to_polygons(p), min_perimeter_infill_spacing))
.size() >= internal_volume.size();
}),
regularized_shell.end());
}
@@ -2980,9 +2876,8 @@ void PrintObject::discover_vertical_shells()
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
// Trim the internal & internalvoid by the shell.
const Polygons regularized_shell_polygons = to_polygons(regularized_shell);
Slic3r::ExPolygons new_internal = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternal)), regularized_shell_polygons);
Slic3r::ExPolygons new_internal_void = diff_ex_by_piece(to_expolygons(layerm->fill_surfaces.filter_by_type(stInternalVoid)), regularized_shell_polygons);
Slic3r::ExPolygons new_internal = diff_ex(layerm->fill_surfaces.filter_by_type(stInternal), regularized_shell);
Slic3r::ExPolygons new_internal_void = diff_ex(layerm->fill_surfaces.filter_by_type(stInternalVoid), regularized_shell);
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
{
@@ -3009,15 +2904,7 @@ void PrintObject::discover_vertical_shells()
layerm->export_region_fill_surfaces_to_svg_debug("3_discover_vertical_shells-final");
}
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
}; // for each region
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);
} // for each region
} // void PrintObject::discover_vertical_shells()
// #define DEBUG_BRIDGE_OVER_INFILL
@@ -3308,7 +3195,7 @@ void PrintObject::bridge_over_infill()
}
this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer);
this->m_adaptive_infill_fields = this->prepare_adaptive_infill_fields();
this->m_tpms_radial_fields = this->prepare_tpms_radial_fields();
std::vector<size_t> layers_to_generate_infill;
for (const auto &pair : surfaces_by_layer) {
@@ -3538,16 +3425,6 @@ void PrintObject::bridge_over_infill()
vertical_lines[i].b = Point{x, y_max};
}
// The vertical lines only span the bridged area's x range, so anchors entirely outside it can never be
// hit. Leaving them out gives the same intersections without building a tree over the whole layer's
// boundary for every bridge.
const coord_t scan_x_min = bb_x.min.x();
const coord_t scan_x_max = bb_x.min.x() + coord_t(n_vlines) * scan_spacing;
anchors.erase(std::remove_if(anchors.begin(), anchors.end(),
[scan_x_min, scan_x_max](const Line &l) {
return std::max(l.a.x(), l.b.x()) < scan_x_min || std::min(l.a.x(), l.b.x()) > scan_x_max;
}),
anchors.end());
auto anchors_and_walls_tree = AABBTreeLines::LinesDistancer<Line>{std::move(anchors)};
auto bridged_area_tree = AABBTreeLines::LinesDistancer<Line>{to_lines(bridged_area)};
@@ -3792,58 +3669,26 @@ void PrintObject::bridge_over_infill()
std::vector<CandidateSurface> expanded_surfaces;
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
// The expanded fill boundary depends only on the bridging flow, and total_fill_area is not
// modified below, so build it once per spacing rather than once per candidate. A layer split
// into many candidates (e.g. by colour painting) otherwise repeats a layer-wide offset for each.
std::map<coord_t, Polylines> boundary_by_spacing;
// expansion_area is a clean, non-overlapping set, so cutting a bridge out of it only changes the
// polygons near that bridge. The rest are passed through untouched instead of being fed to Clipper
// with the whole layer again for every candidate.
// Not `near`/`far`: the Windows headers still define those as macros, and they expand to
// nothing, which turns the declaration below into an empty one.
const auto split_near = [](const Polygons &polys, const BoundingBox &bbox, Polygons &rest) {
Polygons nearby;
for (const Polygon &p : polys)
(get_extents(p).overlap(bbox) ? nearby : rest).emplace_back(p);
return nearby;
};
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
const auto &region_config = candidate.region->region().config();
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
region_config.sparse_infill_pattern == ipOctagramSpiral;
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
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)));
ExPolygons bridge_components = intersection_ex(expand(candidate.new_polys, flow.scaled_spacing()), deep_infill_area);
// Orca: Filter whole bridge areas so their holes remain holes.
bridge_components.erase(std::remove_if(bridge_components.begin(), bridge_components.end(),
[&internal_unsupported_area](const ExPolygon &component) {
return intersection_ex(component, ClipperUtils::clip_clipper_polygons_with_subject_bbox(
internal_unsupported_area,
get_extents(component).inflated(SCALED_EPSILON)))
.empty();
return intersection_ex(component, internal_unsupported_area).empty();
}),
bridge_components.end());
Polygons area_to_be_bridge = to_polygons(std::move(bridge_components));
Polygons limiting_area = union_(area_to_be_bridge, expansion_area);
if (area_to_be_bridge.empty())
continue;
// 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 boundary_plines = to_polylines(expand(total_fill_area, 1.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());
@@ -3919,12 +3764,9 @@ void PrintObject::bridge_over_infill()
// Check collision with other expanded surfaces
{
bool reconstruct = false;
Polygons tmp_expanded_area = expand(bridging_area, 3.0 * flow.scaled_spacing());
const BoundingBox tmp_expanded_bbox = get_extents(tmp_expanded_area);
Polygons tmp_expanded_area = expand(bridging_area, 3.0f * flow.scaled_spacing());
for (const CandidateSurface &s : expanded_surfaces) {
// 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()) {
if (!intersection(s.new_polys, tmp_expanded_area).empty()) {
bridging_angle = s.bridge_angle;
reconstruct = true;
break;
@@ -3948,20 +3790,10 @@ void PrintObject::bridge_over_infill()
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
bridging_angle, scan_spacing, true));
}
// Each of these meets one bridge with the whole layer, so the layer side is first cut down to the
// bridge's box (and expansion_area split as above); the result is the same.
if (!bridging_area.empty()) {
const BoundingBox bridging_bbox = get_extents(bridging_area).inflated(SCALED_EPSILON);
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(limiting_area, bridging_bbox));
bridging_area = intersection(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_fill_area, bridging_bbox));
bridging_area = diff(bridging_area, ClipperUtils::clip_clipper_polygons_with_subject_bbox(total_top_area, bridging_bbox));
}
if (!bridging_area.empty()) {
Polygons kept;
const Polygons cut = split_near(expansion_area, get_extents(bridging_area).inflated(SCALED_EPSILON), kept);
append(kept, diff(cut, bridging_area));
expansion_area = std::move(kept);
}
bridging_area = intersection(bridging_area, limiting_area);
bridging_area = intersection(bridging_area, total_fill_area);
bridging_area = diff(bridging_area, total_top_area);
expansion_area = diff(expansion_area, bridging_area);
#ifdef DEBUG_BRIDGE_OVER_INFILL
debug_draw(std::to_string(lidx) + "_" + std::to_string(cluster_idx) + "_" + std::to_string(job_idx) + "_" + "_expanded_bridging" + std::to_string(r),
+1 -1
View File
@@ -1004,9 +1004,9 @@ public:
::fread(&y, sizeof(coord_t), 1, file);
poly.points.emplace_back(Point(x * scale, y * scale));
}
printf("Polygon %d, area: %lf\n", i, area(poly.points));
if (which == -1 || which == i)
m_support_polygons_deserialized.emplace_back(std::move(poly));
printf("Polygon %d, area: %lf\n", i, area(poly.points));
}
::fread(&n_polygons, 4, 1, file);
m_trimming_polygons_deserialized.reserve(n_polygons);
+7 -37
View File
@@ -996,46 +996,16 @@ void TreeSupport::detect_overhangs(bool check_support_necessity/* = false*/)
if (is_auto(stype) && config_detect_sharp_tails)
{
// BBS detect sharp tail
// On a belt, "below" has to include the belt itself and the
// shear-advanced lower layer, or every belt-contact island reads as
// a sharp tail -- which is what the empty-predecessor skip above was
// really masking. effective_lower is exactly that notion of below.
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
// Each island is tested only against the lower islands whose box meets its own; overlaps() tries
// every pair, which is quadratic in the island counts of the two layers.
std::vector<BoundingBox> lower_bboxes;
lower_bboxes.reserve(tail_lower.size());
for (const ExPolygon &lower : tail_lower)
lower_bboxes.emplace_back(get_extents(lower));
for (const ExPolygon& expoly : curr_polys) {
bool is_sharp_tail = false;
// 1. nothing below
// this is a sharp tail region if it's floating and non-ignorable
const ExPolygons expanded = offset_ex(expoly, 0.1 * extrusion_width_scaled);
const BoundingBox bbox = get_extents(expanded);
ExPolygons lower_nearby;
for (size_t i = 0; i < tail_lower.size(); ++i)
if (lower_bboxes[i].overlap(bbox))
lower_nearby.emplace_back(tail_lower[i]);
// As overlaps(expanded, lower_nearby), with each lower island cut to the island's box first:
// below a fine relief the lower layer is a few islands with thousands of holes, and the whole
// of that boundary would otherwise be intersected once per island above.
const auto overlaps_nearby = [&]() {
for (const ExPolygon &a : expanded) {
if (a.empty())
continue;
const BoundingBox a_bbox = get_extents(a);
for (const ExPolygon &b : lower_nearby) {
if (b.empty() || !get_extents(b).overlap(a_bbox))
continue;
const Polygons b_near = ClipperUtils::clip_clipper_polygons_with_subject_bbox(b, a_bbox.inflated(SCALED_EPSILON));
if (!intersection_pl(to_polylines(b_near), a).empty() || b.contains(a.contour.points.front()))
return true;
}
}
return false;
};
if (!overlaps_nearby()) {
// this is a sharp tail region if it's floating and non-ignorable.
// On a belt, "below" has to include the belt itself and the
// shear-advanced lower layer, or every belt-contact island reads as
// a sharp tail -- which is what the empty-predecessor skip above was
// really masking. effective_lower is exactly that notion of below.
const ExPolygons &tail_lower = belt_ovh_active ? effective_lower : lower_polys;
if (!overlaps(offset_ex(expoly, 0.1 * extrusion_width_scaled), tail_lower)) {
is_sharp_tail = !offset_ex(expoly, -0.1 * extrusion_width_scaled).empty();
}
+2 -2
View File
@@ -68,7 +68,7 @@ public:
thickness(other.thickness), thickness_layers(other.thickness_layers),
bridge_angle(other.bridge_angle), extra_perimeters(other.extra_perimeters)
{};
Surface(Surface &&rhs) noexcept
Surface(Surface &&rhs)
: surface_type(rhs.surface_type), expolygon(std::move(rhs.expolygon)),
thickness(rhs.thickness), thickness_layers(rhs.thickness_layers),
bridge_angle(rhs.bridge_angle), extra_perimeters(rhs.extra_perimeters)
@@ -94,7 +94,7 @@ public:
return *this;
}
Surface& operator=(Surface &&rhs) noexcept
Surface& operator=(Surface &&rhs)
{
surface_type = rhs.surface_type;
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())
dest = std::move(src);
else
// insert() grows the capacity geometrically; reserving exactly the new size reallocated on every call, which
// made appending piece by piece quadratic.
dest.insert(dest.end(), std::make_move_iterator(src.begin()), std::make_move_iterator(src.end()));
else {
dest.reserve(dest.size() + src.size());
std::move(std::begin(src), std::end(src), std::back_inserter(dest));
}
src.clear();
src.shrink_to_fit();
}
+8 -8
View File
@@ -843,17 +843,17 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in
bool have_multiline_infill_pattern = pattern == ipGyroid || pattern == ipGrid || pattern == ipRectilinear || pattern == ipTpmsD || pattern == ipTpmsFK || pattern == ipCrossHatch || pattern == ipHoneycomb || pattern == ipLateralLattice || pattern == ipLateralHoneycomb || pattern == ipConcentric ||
pattern == ipCubic || pattern == ipStars || pattern == ipAlignedRectilinear || pattern == ipLightning || pattern == ip3DHoneycomb || pattern == ipAdaptiveCubic || pattern == ipSupportCubic|| pattern == ipTriangles || pattern == ipQuarterCubic|| pattern == ipArchimedeanChords || pattern == ipHilbertCurve || pattern == ipOctagramSpiral;
// The sparse infill density is the surface density of the adaptive infill; at 100% the infill is solid.
bool have_adaptive_infill_pattern = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
is_adaptive_infill_pattern(pattern);
toggle_line("adaptive_infill_mode", have_adaptive_infill_pattern);
bool have_adaptive_infill = have_adaptive_infill_pattern && config->opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") != AdaptiveInfillMode::Disabled;
toggle_line("adaptive_infill_interior_density", have_adaptive_infill);
toggle_line("adaptive_infill_gradient", have_adaptive_infill);
// The sparse infill density is the surface density of the adaptive TPMS infill; at 100% the infill is solid.
bool have_tpms_infill = have_infill && config->option<ConfigOptionPercent>("sparse_infill_density")->value < 100 &&
is_tpms_adaptive_pattern(pattern);
toggle_line("tpms_adaptive", have_tpms_infill);
bool have_tpms_adaptive = have_tpms_infill && config->opt_enum<TpmsAdaptiveMode>("tpms_adaptive") != TpmsAdaptiveMode::Disabled;
toggle_line("tpms_interior_density", have_tpms_adaptive);
toggle_line("tpms_adaptive_gradient", have_tpms_adaptive);
// gyroid_optimized only applies when the sparse infill pattern is gyroid without adaptive density;
// hide the whole line otherwise.
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_adaptive_infill);
toggle_line("gyroid_optimized", have_infill && pattern == ipGyroid && !have_tpms_adaptive);
// If there is infill, enable/disable fill_multiline according to whether the pattern supports multiline infill.
if (have_infill) {
+3 -3
View File
@@ -149,9 +149,9 @@ std::map<std::string, std::vector<SimpleSettingData>> SettingsFactory::PART_CATE
{"sparse_infill_density", "", 1},
{"fill_multiline", "", 1},
{"sparse_infill_pattern", "", 1},
{"adaptive_infill_mode", "", 1},
{"adaptive_infill_interior_density", "", 1},
{"adaptive_infill_gradient", "", 1},
{"tpms_adaptive", "", 1},
{"tpms_interior_density", "", 1},
{"tpms_adaptive_gradient", "", 1},
{"sparse_infill_smooth_factor", "", 1},
{"lateral_lattice_angle_1", "", 1},
{"lateral_lattice_angle_2", "", 1},
+1 -45
View File
@@ -228,7 +228,6 @@
#include "Jobs/PlaterWorker.hpp"
#include "Jobs/BoostThreadWorker.hpp"
#include "BackgroundSlicingProcess.hpp"
#include "PostProcessor.hpp"
#include "SelectMachine.hpp"
#include "SendMultiMachinePage.hpp"
#include "SendToPrinter.hpp"
@@ -19573,11 +19572,7 @@ void Plater::export_gcode_3mf(bool export_all)
SaveStrategy strategy = SaveStrategy::Silence | SaveStrategy::SplitModel | SaveStrategy::WithGcode | SaveStrategy::SkipModel; // BBS: silence
if (wxGetApp().app_config->get_bool("export_statistics"))
strategy = strategy | SaveStrategy::WithStatistics;
if (export_3mf(output_path, strategy, plate_idx) < 0) {
// No file was written (e.g. a post-processing script failed), don't report a successful export.
p->exporting_status = ExportingStatus::NOT_EXPORTING;
return;
}
export_3mf(output_path, strategy, plate_idx);
RemovableDriveManager& removable_drive_manager = *wxGetApp().removable_drive_manager();
@@ -20434,45 +20429,6 @@ int Plater::export_3mf(const boost::filesystem::path& output_path, SaveStrategy
p->partplate_list.store_to_3mf_structure(plate_data_list, (strategy & SaveStrategy::WithGcode || strategy & SaveStrategy::WithSliceInfo), export_plate_idx,
strategy & SaveStrategy::WithStatistics);
// Orca: non-BBL G-code isn't post-processed in place, so run the scripts on a copy before packing it.
std::vector<std::string> pp_tmp_files;
ScopeGuard remove_pp_tmp_files([&pp_tmp_files]() {
for (const std::string& f : pp_tmp_files) {
boost::system::error_code ec;
boost::filesystem::remove(f, ec);
}
});
// Backups (SkipStatic) don't pack G-code, so don't run user scripts for them.
if ((strategy & SaveStrategy::WithGcode) && !(strategy & SaveStrategy::SkipStatic) &&
p->printer_technology == ptFFF && !wxGetApp().preset_bundle->is_bbl_vendor()) {
try {
for (PlateData* pd : plate_data_list) {
if (pd == nullptr || pd->gcode_file.empty())
continue;
PartPlate* plate = p->partplate_list.get_plate(pd->plate_index);
if (plate == nullptr)
continue;
PrintBase* print = nullptr;
plate->get_print(&print, nullptr, nullptr);
Print* fff_print = dynamic_cast<Print*>(print);
if (fff_print == nullptr)
continue;
std::string src_path = pd->gcode_file; // in/out: becomes the post-processed copy if a script ran
std::string output_name = path_u8; // scripts see the real target; a rename is ignored for 3mf
if (run_post_process_scripts(src_path, true, "File", output_name, fff_print->full_print_config())) {
pp_tmp_files.push_back(src_path);
pd->gcode_file = src_path;
}
}
} catch (const std::exception& ex) {
release_PlateData_list(plate_data_list);
show_error(this, ex.what(), false);
return -1;
}
}
// BBS: backup
PresetBundle& preset_bundle = *wxGetApp().preset_bundle;
std::vector<Preset*> project_presets = preset_bundle.get_current_project_embedded_presets();
+3 -3
View File
@@ -2968,9 +2968,9 @@ void TabPrint::build()
optgroup->append_single_option_line("sparse_infill_density", "strength_settings_infill#sparse-infill-density");
optgroup->append_single_option_line("fill_multiline", "strength_settings_infill#fill-multiline");
optgroup->append_single_option_line("sparse_infill_pattern", "strength_settings_infill#sparse-infill-pattern");
optgroup->append_single_option_line("adaptive_infill_mode", "strength_settings_patterns#adaptive-density");
optgroup->append_single_option_line("adaptive_infill_interior_density", "strength_settings_patterns#interior-density");
optgroup->append_single_option_line("adaptive_infill_gradient", "strength_settings_patterns#adaptive-gradient");
optgroup->append_single_option_line("tpms_adaptive", "strength_settings_patterns#adaptive-density");
optgroup->append_single_option_line("tpms_interior_density", "strength_settings_patterns#interior-density");
optgroup->append_single_option_line("tpms_adaptive_gradient", "strength_settings_patterns#adaptive-gradient");
optgroup->append_single_option_line("gyroid_optimized", "strength_settings_patterns#gyroid-optimized");
optgroup->append_single_option_line("sparse_infill_smooth_factor", "strength_settings_infill#sparse-infill-smooth-factor");
optgroup->append_single_option_line("infill_direction", "strength_settings_infill#direction");
+340 -133
View File
@@ -11,6 +11,7 @@
#include <boost/log/trivial.hpp>
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstddef>
#include <cstdlib>
@@ -130,6 +131,20 @@ static AuditEventCategory event_category(const std::string& event_name)
return it == audit_event_categories.end() ? AuditEventCategory::None : it->second;
}
// Events the 3.12 stdlib raises with sys.audit() inside the function that performs the whole
// operation, so the raising frame bounds everything the operation does. Every other mapped event
// is a primitive raised from C, whose current frame is whatever Python code called it -- possibly
// a library loop over many targets -- so it never marks an operation. glob.glob and glob.glob/2
// are raised in iglob(), which returns its lazy generator before any directory is listed.
static const std::unordered_set<std::string> operation_root_events{
"urllib.Request", "http.client.connect", "os.walk", "os.fwalk",
"glob.glob", "glob.glob/2", "pathlib.Path.glob", "pathlib.Path.rglob",
"subprocess.Popen", "pty.spawn", "tempfile.mkstemp", "tempfile.mkdtemp",
"shutil.copyfile", "shutil.copymode", "shutil.copystat", "shutil.copytree",
"shutil.chown", "shutil.move", "shutil.rmtree", "shutil.make_archive",
"shutil.unpack_archive",
};
// True for the categories whose targets are filesystem paths, as opposed to a network
// address or a process command line -- the deny-path and allowed-root checks only make sense
// against a path.
@@ -146,6 +161,24 @@ static bool is_fs_category(AuditEventCategory category)
}
}
// The category's name in a saved event grant, e.g. "FsRead" in "FsRead:open".
static const char* category_name(AuditEventCategory category)
{
switch (category) {
case AuditEventCategory::FsRead: return "FsRead";
case AuditEventCategory::FsReadWrite: return "FsReadWrite";
case AuditEventCategory::FsCreate: return "FsCreate";
case AuditEventCategory::FsDelete: return "FsDelete";
case AuditEventCategory::Http: return "Http";
case AuditEventCategory::Socket: return "Socket";
case AuditEventCategory::ProcessCreate: return "ProcessCreate";
case AuditEventCategory::ProcessReplace: return "ProcessReplace";
case AuditEventCategory::Threading: return "Threading";
case AuditEventCategory::None: break;
}
return "None";
}
// ---------------------------------------------------------------------------
// Path safety
// ---------------------------------------------------------------------------
@@ -215,26 +248,50 @@ 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_capability_name = "";
thread_local std::vector<AllowedRoot> PluginAuditManager::m_scoped_allowed_roots;
thread_local std::vector<PyObject*> PluginAuditManager::m_root_frames;
thread_local int PluginAuditManager::m_releasing_frames = 0;
thread_local bool PluginAuditManager::m_audit_denial_pending = false;
thread_local bool PluginAuditManager::m_has_last_violation = false;
thread_local AuditViolation PluginAuditManager::m_last_violation;
ScopedPluginAuditContext::ScopedPluginAuditContext(const std::string& plugin_key,
const std::string& capability_name)
: m_previous_id(PluginAuditManager::instance().current_plugin())
: m_plugin_key(plugin_key)
, m_capability(capability_name)
, m_previous_id(PluginAuditManager::instance().current_plugin())
, m_previous_capability(PluginAuditManager::instance().current_capability())
, m_previous_scoped_roots(PluginAuditManager::m_scoped_allowed_roots)
, m_previous_root_frames(std::move(PluginAuditManager::m_root_frames))
{
PluginAuditManager::instance().set_current_plugin(plugin_key);
PluginAuditManager::instance().set_current_capability(capability_name);
PluginAuditManager::m_scoped_allowed_roots.clear();
PluginAuditManager::m_root_frames.clear();
}
ScopedPluginAuditContext::~ScopedPluginAuditContext()
{
PluginAuditManager::instance().set_current_plugin(m_previous_id);
PluginAuditManager::instance().set_current_capability(m_previous_capability);
PluginAuditManager& mgr = PluginAuditManager::instance();
// Let this scope's root frames go while it is still the current context, and as its own
// plugin: the trampoline's denial path clears the key before the scope ends, and a finalizer
// must never be judged as the outer plugin, or bypass the audit when there is none. A
// finalizer may record a new root frame, so repeat until none is held.
if (!PluginAuditManager::m_root_frames.empty()) {
assert(PyGILState_Check());
mgr.set_current_plugin(m_plugin_key);
mgr.set_current_capability(m_capability);
while (!PluginAuditManager::m_root_frames.empty()) {
std::vector<PyObject*> frames = std::move(PluginAuditManager::m_root_frames);
PluginAuditManager::m_root_frames.clear();
mgr.release_frames(frames);
}
}
mgr.set_current_plugin(m_previous_id);
mgr.set_current_capability(m_previous_capability);
PluginAuditManager::m_scoped_allowed_roots = std::move(m_previous_scoped_roots);
PluginAuditManager::m_root_frames = std::move(m_previous_root_frames);
}
// ---------------------------------------------------------------------------
@@ -575,34 +632,6 @@ bool PluginAuditManager::last_violation(AuditViolation& violation) const
return true;
}
bool PluginAuditManager::has_approved_ancestor(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids) const
{
if (plugin_key.empty() || call_site_ids.empty())
return false;
std::lock_guard<std::mutex> lock(m_mutex);
auto it = m_approved_call_sites.find(plugin_key);
if (it == m_approved_call_sites.end())
return false;
for (const auto& id : call_site_ids)
if (it->second.count(id))
return true;
return false;
}
void PluginAuditManager::record_approved_call_sites(const std::string& plugin_key,
const std::vector<std::string>& call_site_ids)
{
if (plugin_key.empty() || call_site_ids.empty())
return;
std::lock_guard<std::mutex> lock(m_mutex);
auto& approved = m_approved_call_sites[plugin_key];
approved.insert(call_site_ids.begin(), call_site_ids.end());
}
// ---------------------------------------------------------------------------
// The C-level audit hook
// ---------------------------------------------------------------------------
@@ -663,40 +692,88 @@ std::string python_str(PyObject* object)
return result;
}
std::vector<std::string> call_site_identities(const std::string& plugin_root)
{
std::vector<std::string> ids;
if (plugin_root.empty())
return ids;
// The interpreter's stdlib folders, and the site-packages folders inside them that hold
// third-party code, both resolved. Set by install_hook(); like the verdict cache below, only
// touched while the GIL is held.
static std::vector<boost::filesystem::path> s_stdlib_dirs;
static std::vector<boost::filesystem::path> s_site_package_dirs;
static std::unordered_map<std::string, bool> s_stdlib_file_verdicts; // co_filename -> is stdlib
void collect_stdlib_dirs()
{
s_stdlib_dirs.clear();
s_site_package_dirs.clear();
s_stdlib_file_verdicts.clear();
PyObject* sysconfig = PyImport_ImportModule("sysconfig");
PyObject* paths = sysconfig ? PyObject_CallMethod(sysconfig, "get_paths", nullptr) : nullptr;
if (paths && PyDict_Check(paths)) {
const auto collect = [paths](const char* key, std::vector<boost::filesystem::path>& out) {
const std::string dir = python_unicode(PyDict_GetItemString(paths, key)); // borrowed
if (dir.empty())
return;
boost::system::error_code ec;
const boost::filesystem::path resolved = boost::filesystem::weakly_canonical(dir, ec);
out.push_back(ec ? boost::filesystem::path(dir) : resolved);
};
collect("stdlib", s_stdlib_dirs);
collect("platstdlib", s_stdlib_dirs);
collect("purelib", s_site_package_dirs);
collect("platlib", s_site_package_dirs);
}
Py_XDECREF(paths);
Py_XDECREF(sysconfig);
if (PyErr_Occurred()) {
PyErr_Clear();
BOOST_LOG_TRIVIAL(error) << "[AUDIT] Failed to read the stdlib folders from sysconfig";
}
}
bool is_stdlib_frame(PyObject* frame)
{
PyCodeObject* code = PyFrame_GetCode(reinterpret_cast<PyFrameObject*>(frame)); // new reference
const std::string filename = python_unicode(code->co_filename);
Py_DECREF(code);
return PluginAuditManager::is_stdlib_file(filename);
}
// The frames on this thread's Python stack, innermost first. The pointers stay valid while the
// caller runs, because a running activation keeps its frame object alive.
std::vector<PyObject*> stack_frames()
{
std::vector<PyObject*> frames;
PyFrameObject* frame = PyEval_GetFrame(); // borrowed reference
Py_XINCREF(frame); // normalize to an owned reference for the loop below
while (frame) {
PyCodeObject* code = PyFrame_GetCode(frame); // new reference
const std::string filename = python_unicode(reinterpret_cast<PyObject*>(code->co_filename));
const bool is_plugin_frame = !filename.empty() && boost::algorithm::starts_with(filename, plugin_root);
std::string id;
if (!is_plugin_frame && !filename.empty()) {
const std::string funcname = python_unicode(reinterpret_cast<PyObject*>(code->co_name));
id = filename + ":" + funcname + ":" + std::to_string(code->co_firstlineno);
}
Py_DECREF(code);
frames.push_back(reinterpret_cast<PyObject*>(frame));
PyFrameObject* back = PyFrame_GetBack(frame); // new reference, or nullptr at the top of the stack
Py_DECREF(frame);
frame = back;
if (is_plugin_frame)
break;
if (!id.empty())
ids.push_back(std::move(id));
}
return frames;
}
Py_XDECREF(frame); // only holds a reference here if the loop exited via break
// Moves the held root frames whose operation has returned from `held` to `dropped`. A suspended
// generator's frame (os.walk between two items) is off the stack, but its operation resumes, so
// it stays held until the scope ends.
void take_returned_frames(std::vector<PyObject*>& held, std::vector<PyObject*>& dropped)
{
if (held.empty())
return;
return ids;
const std::vector<PyObject*> stack = stack_frames();
const auto running = [&stack](PyObject* frame) {
if (std::find(stack.begin(), stack.end(), frame) != stack.end())
return true;
// NULL once the generator has finished or been deleted: its frame then owns its own data.
PyObject* generator = PyFrame_GetGenerator(reinterpret_cast<PyFrameObject*>(frame)); // new reference
const bool suspended = generator != nullptr;
Py_XDECREF(generator);
return suspended;
};
const auto returned = std::stable_partition(held.begin(), held.end(), running);
dropped.insert(dropped.end(), returned, held.end());
held.erase(returned, held.end());
}
static const std::unordered_set<std::string> two_path_fs_events{
@@ -717,12 +794,15 @@ static const std::unordered_map<std::string, std::vector<Py_ssize_t>> audit_targ
{"socket.getnameinfo", {0}},
{"socket.getservbyname", {0}},
{"socket.getservbyport", {0}},
{"socket.sendto", {1}},
{"socket.sendmsg", {1}},
{"os.system", {0}},
{"subprocess.Popen", {1, 0}},
{"os.posix_spawn", {1, 0}},
{"os.spawn", {2, 1}},
{"os.startfile", {0}},
{"os.startfile/2", {0}},
{"pty.spawn", {0}},
{"_winapi.CreateProcess", {1, 0}},
{"_posixsubprocess.fork_exec", {0}},
@@ -767,7 +847,11 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
const auto it = audit_target_arg_indices.find(event_name);
if (it != audit_target_arg_indices.end()) {
for (const Py_ssize_t index : it->second) {
std::string value = python_str(tuple_item(args, index));
// None is an argument left out (sendmsg without an address), not a target named "None".
PyObject* const item = tuple_item(args, index);
if (item == Py_None)
continue;
std::string value = python_str(item);
if (!value.empty()) {
targets.push_back(std::move(value));
break;
@@ -778,8 +862,14 @@ std::vector<std::string> audit_targets(const std::string& event_name, AuditEvent
return targets;
}
std::vector<std::string>* permission_list_for(AuditEventCategory category, PluginPermissions& permissions)
// 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;
@@ -792,6 +882,13 @@ std::vector<std::string>* permission_list_for(AuditEventCategory category, Plugi
}
}
// 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();
@@ -902,22 +999,25 @@ bool prompt_for_targets(AuditEventCategory category, const std::string& plugin_n
// 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, const std::vector<std::string>& targets)
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, state.permissions);
std::vector<std::string>* permission_list = permission_list_for(category, by_event, state.permissions);
if (!permission_list)
return;
for (const auto& target : targets)
persist_permission(plugin_key, state, *permission_list, target);
for (const auto& key : keys)
persist_permission(plugin_key, state, *permission_list, key);
}
// Requests permission for an audited event, returning true when it is already granted or the user
// approves an inline prompt.
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
@@ -929,56 +1029,205 @@ void persist_grant(const std::string& plugin_key, AuditEventCategory category, c
// 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, const std::vector<std::string>& unresolved)
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, unresolved);
return prompt_for_targets(category, plugin_name, event_name, shown);
GUI::wxGetApp().CallAfter([category, plugin_key, plugin_name, event_name, unresolved]() {
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, unresolved))
persist_grant(plugin_key, category, unresolved);
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,
std::vector<std::string>* permission_list,
const std::vector<std::string>& call_site_ids)
const std::vector<std::string>& targets)
{
std::vector<std::string> unresolved = 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& target : targets)
if (!has_permission(*permission_list, target))
unresolved.push_back(target);
if (!targets.empty() && unresolved.empty())
for (const auto& key : keys)
if (!has_permission(*permission_list, key))
unresolved.push_back(key);
if (unresolved.empty())
return 0;
}
if (!request_permission(category, plugin_key, plugin_name, event_name, unresolved))
// 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& target : unresolved)
persist_permission(plugin_key, state, *permission_list, target);
mgr.record_approved_call_sites(plugin_key, call_site_ids);
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.
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 std::vector<std::string> targets = audit_targets(event_name, event_type, args);
// 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
// operation can launder access to one, and before the allowed-root shortcut further down,
// so an allowed root (e.g. the read-only resources folder) cannot make a denied path
// underneath it reachable.
if (fs_category) {
for (const auto& target : targets) {
if (mgr.is_denied_path(boost::filesystem::path(target)))
return report_denied(mgr, event_name, {false, "denied path"}, target);
}
}
// A nested event of an operation already granted, e.g. socket.connect inside an approved
// urlopen(), is not asked again. urllib.Request is always judged on its own URL, so a
// redirect or a nested urlopen() inside a running request is not covered by it.
if (event_name != "urllib.Request" && mgr.covered_by_root_frame())
return 0;
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
// installed/system profile cache), the read-only bundled resources folder, or a per-call
// scoped root such as the current G-code folder -- is part of the plugin system's normal
// workflow and does not need a prompt.
if (fs_category && !targets.empty()) {
const bool is_write = event_type != AuditEventCategory::FsRead;
const bool all_inside_allowed_root =
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
return mgr.check_path_access(boost::filesystem::path(target), is_write).allowed;
});
if (all_inside_allowed_root)
return 0;
}
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
// operation, so it covers nothing else.
if (result == 0 && !targets.empty() && operation_root_events.count(event_name))
mgr.record_root_frame();
return result;
}
} // namespace PluginAuditDetail
void PluginAuditManager::record_root_frame()
{
PyObject* frame = reinterpret_cast<PyObject*>(PyEval_GetFrame()); // borrowed reference
if (!frame || !PluginAuditDetail::is_stdlib_frame(frame))
return;
if (std::find(m_root_frames.begin(), m_root_frames.end(), frame) != m_root_frames.end())
return; // glob.glob and glob.glob/2 share a frame
Py_INCREF(frame);
m_root_frames.push_back(frame);
}
bool PluginAuditManager::covered_by_root_frame() const
{
if (m_root_frames.empty())
return false;
// Only stdlib frames may sit between the event and the operation: plugin or library code
// the operation calls back into (an os.walk onerror, a urllib handler) acts on its own.
for (PyObject* frame : PluginAuditDetail::stack_frames()) {
if (std::find(m_root_frames.begin(), m_root_frames.end(), frame) != m_root_frames.end())
return true;
if (!PluginAuditDetail::is_stdlib_frame(frame))
return false;
}
return false;
}
bool PluginAuditManager::releasing_frames() const { return m_releasing_frames > 0; }
bool PluginAuditManager::is_stdlib_file(const std::string& filename)
{
// Frozen stdlib modules (os, posixpath, codecs, ...) carry a pseudo filename.
if (boost::algorithm::starts_with(filename, "<frozen "))
return true;
if (filename.empty() || filename.front() == '<')
return false;
// Both sides are resolved: co_filename follows the Python home as it was given, which may
// be a symlink.
const auto [it, inserted] = PluginAuditDetail::s_stdlib_file_verdicts.try_emplace(filename, false);
if (inserted) {
const boost::filesystem::path file(filename);
const auto inside_any = [&file](const std::vector<boost::filesystem::path>& dirs) {
return std::any_of(dirs.begin(), dirs.end(),
[&file](const boost::filesystem::path& dir) { return is_inside_allowed_root(file, dir); });
};
it->second = inside_any(PluginAuditDetail::s_stdlib_dirs) && !inside_any(PluginAuditDetail::s_site_package_dirs);
}
return it->second;
}
void PluginAuditManager::release_frames(std::vector<PyObject*>& frames)
{
if (frames.empty())
return;
// A finalizer's own denial must neither replace the PermissionError of the event being
// decided nor leave a stale denial behind for the next call.
PyObject* const error = PyErr_GetRaisedException();
const bool denial_pending = m_audit_denial_pending;
const bool has_violation = m_has_last_violation;
const AuditViolation violation = m_last_violation;
std::vector<PyObject*> released;
released.swap(frames);
++m_releasing_frames;
for (PyObject* frame : released)
Py_DECREF(frame);
--m_releasing_frames;
m_audit_denial_pending = denial_pending;
m_has_last_violation = has_violation;
m_last_violation = violation;
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)
{
auto* mgr = static_cast<PluginAuditManager*>(user_data);
@@ -1006,62 +1255,20 @@ int PluginAuditManager::audit_hook(const char* event, PyObject* args, void* user
if (event_type == AuditEventCategory::None)
return 0;
const bool fs_category = is_fs_category(event_type);
const std::vector<std::string> targets = PluginAuditDetail::audit_targets(event_name, event_type, args);
// A denied path (secrets, certificates, config files -- see is_denied_path) is an
// unconditional block: checked before the ancestor-cascade check below, so a cascade
// approval recorded for an unrelated action can never launder access to one, and before
// the allowed-root shortcut further down, so an allowed root (e.g. the read-only resources
// folder) cannot make a denied path underneath it reachable.
if (fs_category) {
for (const auto& target : targets) {
if (mgr->is_denied_path(boost::filesystem::path(target)))
return PluginAuditDetail::report_denied(*mgr, event_name, {false, "denied path"}, target);
}
}
PluginDescriptor plugin_descriptor;
PluginManager::instance().try_get_plugin_descriptor(mgr->current_plugin(), plugin_descriptor);
// Some plugins call other audited events, e.g. urlib.request will call socket.connect. So this is so that if urlib.request
// was already approved, socket.connect won't trigger another permission dialog.
const std::vector<std::string> call_site_ids =
PluginAuditDetail::call_site_identities(plugin_descriptor.plugin_root);
if (mgr->has_approved_ancestor(mgr->current_plugin(), call_site_ids))
return 0;
// A filesystem target that resolves entirely inside a pre-determined allowed root -- the
// plugin system's own data_dir() tree (which holds each plugin's storage folder and the
// installed/system profile cache), the read-only bundled resources folder, or a per-call
// scoped root such as the current G-code folder -- is part of the plugin system's normal
// workflow and does not need a prompt.
if (fs_category && !targets.empty()) {
const bool is_write = event_type != AuditEventCategory::FsRead;
const bool all_inside_allowed_root =
std::all_of(targets.begin(), targets.end(), [&](const std::string& target) {
return mgr->check_path_access(boost::filesystem::path(target), is_write).allowed;
});
if (all_inside_allowed_root)
return 0;
}
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;
std::vector<std::string>* permission_list = PluginAuditDetail::permission_list_for(event_type, state.permissions);
return PluginAuditDetail::decide_audited_event(*mgr, state, mgr->current_plugin(), plugin_name, event_name,
event_type, targets, permission_list, call_site_ids);
// Root frames whose operation has returned are let go once this event is decided, so that a
// finalizer they run re-enters a hook whose held frames are already consistent.
std::vector<PyObject*> returned_frames;
PluginAuditDetail::take_returned_frames(m_root_frames, returned_frames);
const int result = PluginAuditDetail::audit_plugin_event(*mgr, event_name, event_type, args);
mgr->release_frames(returned_frames);
return result;
}
void PluginAuditManager::install_hook()
{
// Tells the stdlib's own frames apart, for the operation-root frames (see record_root_frame).
PluginAuditDetail::collect_stdlib_dirs();
// data_dir() is the primary globally-allowed root during plugin execution: read+write. It
// covers the plugin system's own workflow needs -- each plugin's storage folder
// (data_dir()/orca_plugins) and the installed/system profile cache (data_dir()/system) --
+34 -13
View File
@@ -4,10 +4,9 @@
// Via pybind11 so this file requests the same python3xx.lib as everything else.
#include <boost/filesystem/path.hpp>
#include <pybind11/conduit/wrap_include_python_h.h>
#include <functional>
#include <mutex>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include <boost/filesystem.hpp>
@@ -144,15 +143,26 @@ public:
void clear_last_violation();
bool last_violation(AuditViolation& violation) const;
// --- call-site cascade cache ---
// A single plugin action often fires several nested CPython audit events as it passes
// through stdlib layers (urllib.request calling http.client calling socket, for example).
// Once the user approves one event, every stdlib frame still on the stack for that call
// is recorded here by (filename, function, first line) identity. A later event whose own
// ancestor chain still contains one of those frames is the same logical action seen from
// a deeper layer, so it is auto-approved instead of prompting again.
bool has_approved_ancestor(const std::string& plugin_key, const std::vector<std::string>& call_site_ids) const;
void record_approved_call_sites(const std::string& plugin_key, const std::vector<std::string>& call_site_ids);
// --- operation-root frames ---
// One stdlib operation fires several nested audit events as it passes through lower layers:
// urlopen() raises urllib.Request, then http.client and socket events. When an operation's
// own root event is granted (by a saved grant or the user), the stdlib frame that raised it
// is held, and a later event whose stack reaches that frame through stdlib frames only is
// part of the same operation, so it is allowed without a prompt of its own. Plugin or
// library code the operation calls back into is judged on its own. A frame is let go once it
// returns, a suspended generator's (os.walk) when the audit scope ends. Letting a frame go may
// run finalizers of objects its locals held; they are judged as the plugin and never prompt.
void record_root_frame();
bool covered_by_root_frame() const;
bool releasing_frames() const;
// True when code from `filename` (a code object's co_filename) is the interpreter's own
// stdlib: the only code a root frame's coverage passes through. Valid after install_hook().
static bool is_stdlib_file(const std::string& filename);
// Answers permission prompts in place of the dialog, for tests. Production never sets it.
using PermissionPrompt = std::function<bool(const std::string& event_name, const std::vector<std::string>& targets)>;
static void set_permission_prompt_for_testing(PermissionPrompt prompt);
bool verbose_events = true;
@@ -163,24 +173,32 @@ private:
static int audit_hook(const char* event, PyObject* args, void* user_data);
// Drops the references in `frames`, keeping the caller's error and denial state intact.
void release_frames(std::vector<PyObject*>& frames);
static thread_local std::string m_current_plugin_key;
static thread_local std::string m_current_capability_name;
static thread_local std::vector<AllowedRoot> m_scoped_allowed_roots;
static thread_local std::vector<PyObject*> m_root_frames; // strong references
static thread_local int m_releasing_frames;
static thread_local bool m_audit_denial_pending;
static thread_local bool m_has_last_violation;
static thread_local AuditViolation m_last_violation;
// mutable: is_denied_filename() and has_approved_ancestor() are const queries that must lock.
// mutable: is_denied_filename() is a const query that must lock.
mutable std::mutex m_mutex;
std::vector<AllowedRoot> m_global_allowed_roots;
std::vector<std::string> m_denied_filenames;
std::vector<std::string> m_denied_path_keywords;
std::unordered_map<std::string, std::unordered_set<std::string>> m_approved_call_sites; // plugin_key -> call-site ids
};
// RAII guard that sets the current plugin key and capability name, restoring the previous
// pair on scope exit. `capability_name` may be empty for calls that are not scoped to a
// single capability.
//
// Each scope owns the root frames recorded inside it and lets them go, as its own plugin, before
// restoring the outer context. That needs the GIL whenever a frame is still held, so construct
// the scope after the GIL guard, to be destroyed before it.
class ScopedPluginAuditContext
{
public:
@@ -194,9 +212,12 @@ public:
ScopedPluginAuditContext& operator=(const ScopedPluginAuditContext&) = delete;
private:
std::string m_plugin_key;
std::string m_capability;
std::string m_previous_id;
std::string m_previous_capability;
std::vector<AllowedRoot> m_previous_scoped_roots;
std::vector<PyObject*> m_previous_root_frames;
};
} // namespace Slic3r
+2
View File
@@ -808,6 +808,7 @@ bool read_install_state(const boost::filesystem::path& plugin_dir, PluginInstall
read_string_list("network_socket", parsed.permissions.network_socket);
read_string_list("process", parsed.permissions.process);
read_string_list("threading", parsed.permissions.threading);
read_string_list("events", parsed.permissions.events);
}
if (state.contains("enabled") && state["enabled"].is_boolean())
@@ -852,6 +853,7 @@ bool write_install_state(const boost::filesystem::path& plugin_dir, const Plugin
{"network_socket", state.permissions.network_socket},
{"process", state.permissions.process},
{"threading", state.permissions.threading},
{"events", state.permissions.events},
};
nlohmann::json capabilities = nlohmann::json::array();
+2
View File
@@ -104,6 +104,8 @@ struct PluginPermissions
std::vector<std::string> network_socket;
std::vector<std::string> process;
std::vector<std::string> threading;
// Grants for audit events that expose no target, keyed "<category>:<event>" (e.g. "Socket:socket.__new__").
std::vector<std::string> events;
};
struct PluginInstallState {
+1 -1
View File
@@ -102,9 +102,9 @@ public:
// mistake), both fall back to the base's empty object rather than writing `"cap_config": null`.
nlohmann::json get_default_config() const override
{
pybind11::gil_scoped_acquire gil;
ORCA_PY_AUDIT_SCOPE();
try {
pybind11::gil_scoped_acquire gil;
pybind11::function override = pybind11::get_override(static_cast<const Base*>(this), "get_default_config");
if (!override)
return Base::get_default_config();
@@ -282,11 +282,11 @@ public:
int request_bind_ticket(std::string* ticket) override
{
try {
ORCA_PY_AUDIT_SCOPE();
::Slic3r::PluginCapabilityInterface::RefCounter _orca_ref_counter(*this);
::Slic3r::PythonGILState gil;
if (!gil)
throw std::runtime_error("Python interpreter is shutting down");
ORCA_PY_AUDIT_SCOPE();
pybind11::function override =
pybind11::get_override(static_cast<const PrinterAgentPluginCapability*>(this), "request_bind_ticket");
if (!override)
+31 -31
View File
@@ -37,7 +37,7 @@
#include "libslic3r/Fill/Fill.hpp"
#include "libslic3r/Fill/FillAdaptive.hpp"
#include "libslic3r/Fill/FillGyroid.hpp"
#include "libslic3r/Fill/AdaptiveInfillField.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Flow.hpp"
#include "libslic3r/Geometry.hpp"
#include "libslic3r/IntersectionPoints.hpp"
@@ -2060,9 +2060,9 @@ TEST_CASE("Adaptive TPMS infill thins out from the surface to the interior densi
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(60)}, print,
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "25%"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2089,9 +2089,9 @@ TEST_CASE("Adaptive TPMS infill of a tall object is sparsest at its middle heigh
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"},
{"adaptive_infill_mode", "lobes"},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"tpms_adaptive", "lobes"},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
const Polygons core = centered_square(5.);
@@ -2111,9 +2111,9 @@ TEST_CASE("2D adaptive TPMS infill does not change along its axis", "[Fill]")
Slic3r::Test::init_and_process_print({along_x ? make_cube(90., 30., 30.) : make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "25%"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2147,9 +2147,9 @@ TEST_CASE("Adaptive TPMS shells and blend keep the whole deep core of a tall obj
Slic3r::Test::init_and_process_print({make_cube(30., 30., 90.)}, print,
{{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2179,9 +2179,9 @@ TEST_CASE("Adaptive TPMS infill is sparse at the center of both of two united sp
Slic3r::Test::init_and_process_print({TriangleMesh(spheres)}, print,
{{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "25%"},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", "linear"},
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", "linear"},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
};
@@ -2209,9 +2209,9 @@ TEST_CASE("Adaptive TPMS gradients keep the surface density deeper in the order
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(40)}, print,
{{"sparse_infill_pattern", "tpmsd"},
{"sparse_infill_density", "25%"},
{"adaptive_infill_mode", "lobes"},
{"adaptive_infill_interior_density", "5%"},
{"adaptive_infill_gradient", gradient},
{"tpms_adaptive", "lobes"},
{"tpms_interior_density", "5%"},
{"tpms_adaptive_gradient", gradient},
{"layer_height", 0.4},
{"initial_layer_print_height", 0.4}});
return sparse_infill_length(print, 0., 40., centered_square(20.));
@@ -2236,9 +2236,9 @@ TEST_CASE("Adaptive TPMS settings leave the infill unchanged when they do not ap
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "20%"},
{"layer_height", 0.2},
{"adaptive_infill_mode", adaptive},
{"adaptive_infill_interior_density", "40%"},
{"adaptive_infill_gradient", "exponential"}});
{"tpms_adaptive", adaptive},
{"tpms_interior_density", "40%"},
{"tpms_adaptive_gradient", "exponential"}});
const SparseInfillShape expected = sparse_infill_shape(reference);
REQUIRE(expected.path_count > 0);
CHECK(sparse_infill_shape(tuned).sequence == expected.sequence);
@@ -2252,7 +2252,7 @@ TEST_CASE("Adaptive gyroid infill ignores the Z-buckling optimization", "[Fill]"
{{"sparse_infill_pattern", "gyroid"},
{"sparse_infill_density", "10%"},
{"gyroid_optimized", gyroid_optimized},
{"adaptive_infill_mode", "lobes"},
{"tpms_adaptive", "lobes"},
{"layer_height", 0.2}});
return sparse_infill_shape(print);
};
@@ -2268,10 +2268,10 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
// An 80 x 80 x 40 mm box around both regions, which share its radial field.
const ExPolygons box{ExPolygon(Points{Point::new_scale(60., 20.), Point::new_scale(140., 20.), Point::new_scale(140., 100.),
Point::new_scale(60., 100.)})};
std::vector<AdaptiveInfillField::Slice> slices;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 200; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &box});
const AdaptiveInfillField field(slices, get_extents(box), AdaptiveInfillMode::Lobes, [] {});
const TpmsRadialField field(slices, get_extents(box), TpmsAdaptiveMode::Lobes, [] {});
auto circle = [](double radius) {
Polygon contour = make_circle_num_segments(scale_(radius), 120);
@@ -2285,13 +2285,13 @@ TEST_CASE("Adaptive TPMS infill of a region matches the infill of a larger regio
filler->spacing = 0.45;
filler->angle = float(M_PI / 7.);
filler->z = z;
filler->adaptive_infill_field = &field;
filler->tpms_radial_field = &field;
FillParams params;
params.density = 0.2f;
params.adaptive_infill_mode = AdaptiveInfillMode::Lobes;
params.adaptive_infill_interior_density = 0.05f;
params.adaptive_infill_gradient = AdaptiveInfillGradient::Linear;
params.tpms_adaptive = TpmsAdaptiveMode::Lobes;
params.tpms_interior_density = 0.05f;
params.tpms_adaptive_gradient = TpmsAdaptiveGradient::Linear;
params.layer_height = 0.2;
params.dont_adjust = true;
Surface surface(stInternal, expolygon);
@@ -2328,8 +2328,8 @@ TEST_CASE("Adaptive TPMS anchors match the printed infill", "[Fill][InternalBrid
Slic3r::Test::init_and_process_print({Slic3r::Test::cube(30)}, print,
{{"sparse_infill_pattern", pattern},
{"sparse_infill_density", "25%"},
{"adaptive_infill_mode", "lobes"},
{"adaptive_infill_interior_density", "5%"},
{"tpms_adaptive", "lobes"},
{"tpms_interior_density", "5%"},
{"layer_height", 0.2},
{"initial_layer_print_height", 0.2},
{"resolution", 0.012}});
+1 -2
View File
@@ -35,9 +35,8 @@ add_executable(${_TEST_NAME}_tests
test_fill_corner_smoothing.cpp
test_filament_mixer.cpp
test_fill_plane_path.cpp
test_adaptive_infill_field.cpp
test_fill_tpms_adaptive.cpp
test_geometry.cpp
test_kdtree.cpp
test_imex_arrange.cpp
test_imex_helpers.cpp
test_imex_zones.cpp
-90
View File
@@ -7,14 +7,11 @@
#include "libslic3r/libslic3r.h"
#include <numeric>
#include <iostream>
#include <utility>
#include <boost/filesystem.hpp>
#include <utility>
#include <vector>
#include <cstdlib>
#include <catch2/catch_test_macros.hpp>
#include <catch2/generators/catch_generators.hpp>
#include <catch2/catch_approx.hpp>
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
@@ -303,90 +300,3 @@ TEST_CASE("Top level expolygons of an even-odd union", "[ClipperUtils]") {
REQUIRE(area_sum == Catch::Approx(area(top_level) + area(nested)));
REQUIRE(top_level_expolygons(reference).size() == 1);
}
// Rings flattened to x,y,x,y... and sorted, with each ring rotated to start at its lowest point: two
// encodings of the same geometry compare equal however the pieces came back or wherever a ring started.
static std::vector<std::vector<coord_t>> canonical_rings(const ExPolygons &expolygons)
{
std::vector<std::vector<coord_t>> rings;
const auto add = [&rings](const Polygon &poly) {
if (poly.points.empty())
return;
Points pts = poly.points;
std::rotate(pts.begin(),
std::min_element(pts.begin(), pts.end(), [](const Point &a, const Point &b) {
return std::make_pair(a.x(), a.y()) < std::make_pair(b.x(), b.y());
}),
pts.end());
std::vector<coord_t> flat;
flat.reserve(pts.size() * 2);
for (const Point &p : pts) {
flat.emplace_back(p.x());
flat.emplace_back(p.y());
}
rings.emplace_back(std::move(flat));
};
for (const ExPolygon &expoly : expolygons) {
add(expoly.contour);
for (const Polygon &hole : expoly.holes)
add(hole);
}
std::sort(rings.begin(), rings.end());
return rings;
}
// The same rings, every coordinate within `tolerance`.
static bool same_rings(const ExPolygons &a, const ExPolygons &b, coord_t tolerance)
{
const std::vector<std::vector<coord_t>> ra = canonical_rings(a), rb = canonical_rings(b);
return std::equal(ra.begin(), ra.end(), rb.begin(), rb.end(), [tolerance](const std::vector<coord_t> &x, const std::vector<coord_t> &y) {
return std::equal(x.begin(), x.end(), y.begin(), y.end(), [tolerance](coord_t u, coord_t v) { return std::abs(u - v) <= tolerance; });
});
}
TEST_CASE("Tiled diff and intersection return the same polygons as the plain calls", "[ClipperUtils]") {
// A grid of disjoint framed squares, enough of them to be split into several tiles.
const int n = 40;
const coord_t cell = scaled<coord_t>(2.), side = scaled<coord_t>(1.5), frame = scaled<coord_t>(0.3);
ExPolygons subject;
for (int y = 0; y < n; ++ y)
for (int x = 0; x < n; ++ x) {
const Point o(x * cell, y * cell);
ExPolygon square(Polygon({ o, o + Point(side, 0), o + Point(side, side), o + Point(0, side) }));
Polygon hole({ o + Point(frame, frame), o + Point(frame, side - frame), o + Point(side - frame, side - frame), o + Point(side - frame, frame) });
square.holes.emplace_back(std::move(hole));
subject.emplace_back(std::move(square));
}
// Clip polygons crossing many squares, one of them large with holes of its own.
Polygons clip;
const coord_t span = n * cell;
for (int i = 0; i < 8; ++ i) {
const coord_t y0 = coord_t(i) * span / 8, y1 = y0 + scaled<coord_t>(0.9);
clip.emplace_back(Polygon({ Point(- cell, y0), Point(span, y0 + cell * 3), Point(span, y1 + cell * 3), Point(- cell, y1) }));
}
ExPolygon big(Polygon({ Point(span / 4, span / 4), Point(3 * span / 4, span / 4), Point(3 * span / 4, 3 * span / 4), Point(span / 4, 3 * span / 4) }));
for (int i = 0; i < 4; ++ i) {
const Point o(span / 4 + scaled<coord_t>(3.1) + i * scaled<coord_t>(9.7), span / 4 + scaled<coord_t>(5.3));
big.holes.emplace_back(Polygon({ o, o + Point(0, scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), scaled<coord_t>(20.)), o + Point(scaled<coord_t>(5.), 0) }));
}
polygons_append(clip, to_polygons(big));
const ApplySafetyOffset safety = GENERATE(ApplySafetyOffset::No, ApplySafetyOffset::Yes);
// The point of the fixture: below 128 pieces the helpers fall back to a single tile and the tiled
// path under test is never taken.
REQUIRE(ClipperUtils::tile_expolygons(subject, 32).size() > 1);
// With the safety offset a tile unites fewer clip polygons, so Clipper2 can round a crossing 1 unit differently.
const coord_t tolerance = safety == ApplySafetyOffset::Yes ? 1 : 0;
const ExPolygons diff_plain = diff_ex(subject, clip, safety);
const ExPolygons diff_tiled = diff_ex_by_piece(subject, clip, safety);
REQUIRE(area(diff_plain) > 0.);
CHECK(same_rings(diff_tiled, diff_plain, tolerance));
const ExPolygons intersection_plain = intersection_ex(subject, clip, safety);
const ExPolygons intersection_tiled = intersection_ex_by_piece(subject, clip, safety);
REQUIRE(area(intersection_plain) > 0.);
CHECK(same_rings(intersection_tiled, intersection_plain, tolerance));
}
-13
View File
@@ -1457,19 +1457,6 @@ TEST_CASE("min_object_distance yields no floor when an FFF config lacks the opti
}
}
TEST_CASE("handle_legacy migrates the adaptive TPMS keys", "[Config]") {
ConfigSubstitutionContext ctxt(ForwardCompatibilitySubstitutionRule::Enable);
DynamicPrintConfig config;
config.set_deserialize("tpms_adaptive", "stepped_shells", ctxt);
config.set_deserialize("tpms_interior_density", "12%", ctxt);
config.set_deserialize("tpms_adaptive_gradient", "quadratic", ctxt);
CHECK(config.opt_enum<AdaptiveInfillMode>("adaptive_infill_mode") == AdaptiveInfillMode::SteppedShells);
CHECK(config.option<ConfigOptionPercent>("adaptive_infill_interior_density")->value == Catch::Approx(12.));
CHECK(config.opt_enum<AdaptiveInfillGradient>("adaptive_infill_gradient") == AdaptiveInfillGradient::Quadratic);
CHECK(ctxt.substitutions.empty());
}
TEST_CASE("handle_legacy migrates every IDEX/IQEX key testers already have", "[Config]") {
// Three eras of saved profiles: the feature shipped as iXex (is_ixex + ixex_*), the clearance
// pair was renamed to say what it measures, then the whole prefix became imex_. Driven from the
@@ -13,7 +13,7 @@
#include "libslic3r/BoundingBox.hpp"
#include "libslic3r/ClipperUtils.hpp"
#include "libslic3r/ExPolygon.hpp"
#include "libslic3r/Fill/AdaptiveInfillField.hpp"
#include "libslic3r/Fill/FillTpmsAdaptive.hpp"
#include "libslic3r/Point.hpp"
#include "libslic3r/Polygon.hpp"
#include "libslic3r/PrintConfig.hpp"
@@ -30,24 +30,24 @@ ExPolygon rectangle(double x0, double y0, double x1, double y1)
}
// The expolygons stacked in 0.2 mm layers from z = 0 to height.
AdaptiveInfillField radial_field(const ExPolygons &expolygons, double height, AdaptiveInfillMode mode = AdaptiveInfillMode::Lobes)
TpmsRadialField radial_field(const ExPolygons &expolygons, double height, TpmsAdaptiveMode mode = TpmsAdaptiveMode::Lobes)
{
std::vector<AdaptiveInfillField::Slice> slices;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; 0.2 * (i + 1) < height + EPSILON; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &expolygons});
return AdaptiveInfillField(slices, get_extents(expolygons), mode, [] {});
return TpmsRadialField(slices, get_extents(expolygons), mode, [] {});
}
double radial(const AdaptiveInfillField &field, const Vec3d &pt)
double radial(const TpmsRadialField &field, const Vec3d &pt)
{
AdaptiveInfillField::Radials radials;
TpmsRadialField::Radials radials;
field.radial(pt, radials);
return radials[0].t;
}
Vec3d center(const AdaptiveInfillField &field, const Vec3d &pt)
Vec3d center(const TpmsRadialField &field, const Vec3d &pt)
{
AdaptiveInfillField::Radials radials;
TpmsRadialField::Radials radials;
field.radial(pt, radials);
return radials[0].center;
}
@@ -57,11 +57,11 @@ constexpr double Tolerance = 0.075;
} // namespace
TEST_CASE("Adaptive infill field is zero at the center of a cube and one at its faces", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field is zero at the center of a cube and one at its faces", "[FillTpmsAdaptive]")
{
// A 20 mm cube, 10 mm from its center to every face.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const AdaptiveInfillField field = radial_field(square, 20.);
const TpmsRadialField field = radial_field(square, 20.);
const Vec3d c = center(field, {10., 10., 10.});
CHECK_THAT(c.x(), WithinAbs(10., 0.5));
@@ -74,10 +74,10 @@ TEST_CASE("Adaptive infill field is zero at the center of a cube and one at its
}
}
TEST_CASE("Adaptive infill field grows linearly from the center of a cube to its faces", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field grows linearly from the center of a cube to its faces", "[FillTpmsAdaptive]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const AdaptiveInfillField field = radial_field(square, 20.);
const TpmsRadialField field = radial_field(square, 20.);
for (double d = 1.; d < 10.; d += 1.) {
CAPTURE(d);
CHECK_THAT(radial(field, {10. - d, 10., 10.}), WithinAbs(d / 10., Tolerance));
@@ -85,11 +85,11 @@ TEST_CASE("Adaptive infill field grows linearly from the center of a cube to its
}
}
TEST_CASE("Adaptive infill field of a tall box is centered at its middle height", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field of a tall box is centered at its middle height", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const AdaptiveInfillField field = radial_field(square, 60.);
const TpmsRadialField field = radial_field(square, 60.);
CHECK_THAT(center(field, {10., 10., 45.}).z(), WithinAbs(30., 0.5));
CHECK_THAT(radial(field, {10., 10., 15.}), WithinAbs(0.5, Tolerance));
@@ -97,10 +97,10 @@ TEST_CASE("Adaptive infill field of a tall box is centered at its middle height"
CHECK_THAT(radial(field, {15., 10., 30.}), WithinAbs(0.5, Tolerance));
}
TEST_CASE("Adaptive infill field grades every body towards its own center", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field grades every body towards its own center", "[FillTpmsAdaptive]")
{
const ExPolygons squares{rectangle(0., 0., 20., 20.), rectangle(30., 0., 50., 20.)};
const AdaptiveInfillField field = radial_field(squares, 20.);
const TpmsRadialField field = radial_field(squares, 20.);
CHECK_THAT(center(field, {5., 10., 10.}).x(), WithinAbs(10., 0.5));
CHECK_THAT(center(field, {45., 10., 10.}).x(), WithinAbs(40., 0.5));
@@ -108,20 +108,20 @@ TEST_CASE("Adaptive infill field grades every body towards its own center", "[Ad
CHECK_THAT(radial(field, {30., 10., 10.}), WithinAbs(1., Tolerance));
}
TEST_CASE("Adaptive infill field is beyond one outside of the object", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field is beyond one outside of the object", "[FillTpmsAdaptive]")
{
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const AdaptiveInfillField field = radial_field(square, 20.);
const TpmsRadialField field = radial_field(square, 20.);
CHECK(radial(field, {-5., 10., 10.}) > 1.);
CHECK(radial(field, {10., 10., 30.}) > 1.);
}
TEST_CASE("Adaptive infill field grades every lobe of a body towards its own center", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field grades every lobe of a body towards its own center", "[FillTpmsAdaptive]")
{
// Two spheres of 10 mm united, their centers 16 mm apart: the neck between them is 6 mm deep.
const Vec3d c1(10., 10., 10.), c2(26., 10., 10.);
std::vector<ExPolygons> layers;
std::vector<AdaptiveInfillField::Slice> slices;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
@@ -134,7 +134,7 @@ TEST_CASE("Adaptive infill field grades every lobe of a body towards its own cen
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
for (const Vec3d &c : {c1, c2}) {
CAPTURE(c.x());
@@ -143,7 +143,7 @@ TEST_CASE("Adaptive infill field grades every lobe of a body towards its own cen
CHECK_THAT(radial(field, c + Vec3d(0., 0., 9.5)), WithinAbs(1., 2. * Tolerance));
}
// The side between the lobes is half way to the surface, where both patterns morph into each other.
AdaptiveInfillField::Radials radials;
TpmsRadialField::Radials radials;
REQUIRE(field.radial(0.5 * (c1 + c2), radials) == 2);
for (size_t i = 0; i < 2; ++i) {
CHECK_THAT(radials[i].t, WithinAbs(0.5, 2. * Tolerance));
@@ -151,12 +151,12 @@ TEST_CASE("Adaptive infill field grades every lobe of a body towards its own cen
}
}
TEST_CASE("Adaptive infill field blends the lobes meeting at a junction continuously", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field blends the lobes meeting at a junction continuously", "[FillTpmsAdaptive]")
{
// Three spheres of 10 mm united, their centers on a triangle of 16 mm sides: the necks meet at its middle.
const std::array<Vec3d, 3> centers{Vec3d(10., 10., 10.), Vec3d(26., 10., 10.), Vec3d(18., 10. + 8. * std::sqrt(3.), 10.)};
std::vector<ExPolygons> layers;
std::vector<AdaptiveInfillField::Slice> slices;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 100; ++i) {
const double z = 0.2 * i + 0.1, r = std::sqrt(std::max(0., 100. - sqr(z - 10.)));
Polygons circles;
@@ -169,7 +169,7 @@ TEST_CASE("Adaptive infill field blends the lobes meeting at a junction continuo
}
for (int i = 0; i < 100; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &layers[i]});
const AdaptiveInfillField field(slices, get_extents(layers[50]), AdaptiveInfillMode::Lobes, [] {});
const TpmsRadialField field(slices, get_extents(layers[50]), TpmsAdaptiveMode::Lobes, [] {});
// Around the junction the nearest lobes swap, but the weight of every lobe changes smoothly.
const Vec3d junction = (centers[0] + centers[1] + centers[2]) / 3.;
@@ -179,7 +179,7 @@ TEST_CASE("Adaptive infill field blends the lobes meeting at a junction continuo
for (int row = 0; row <= 100; ++row) {
std::array<float, 3> previous{};
for (int step = 0; step <= 200; ++step) {
AdaptiveInfillField::Radials radials;
TpmsRadialField::Radials radials;
const size_t count = field.radial(junction + Vec3d(0.01 * step - 1., 0.02 * row - 1., 0.), radials);
max_count = std::max(max_count, count);
std::array<float, 3> weights{};
@@ -201,23 +201,23 @@ TEST_CASE("Adaptive infill field blends the lobes meeting at a junction continuo
CHECK(max_jump < 0.05);
}
TEST_CASE("Adaptive infill field is empty when the object is thinner than the grid cells", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field is empty when the object is thinner than the grid cells", "[FillTpmsAdaptive]")
{
// A 0.3 mm square bar between the nodes of a grid sized by a 200 mm bounding box, with cells of 0.5 mm or more.
const ExPolygons bar{rectangle(0.1, 0.1, 0.4, 0.4)};
std::vector<AdaptiveInfillField::Slice> slices;
std::vector<TpmsRadialField::Slice> slices;
for (int i = 0; i < 1000; ++i)
slices.push_back({0.2 * i, 0.2 * (i + 1), &bar});
const AdaptiveInfillField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
AdaptiveInfillMode::Lobes, [] {});
const TpmsRadialField field(slices, BoundingBox(Point::new_scale(0., 0.), Point::new_scale(200., 200.)),
TpmsAdaptiveMode::Lobes, [] {});
CHECK(field.empty());
}
TEST_CASE("Adaptive infill depth follows the distance to the surface relative to the deepest point", "[AdaptiveInfillField]")
TEST_CASE("TPMS depth follows the distance to the surface relative to the deepest point", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: from 10 to 50 mm high the axis is 10 mm deep, as deep as the center.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const AdaptiveInfillField field = radial_field(square, 60., AdaptiveInfillMode::SmoothBlend);
const TpmsRadialField field = radial_field(square, 60., TpmsAdaptiveMode::SmoothBlend);
for (double z : {15., 30., 45.}) {
CAPTURE(z);
CHECK_THAT(field.depth({10., 10., z}), WithinAbs(1., Tolerance));
@@ -227,18 +227,18 @@ TEST_CASE("Adaptive infill depth follows the distance to the surface relative to
CHECK_THAT(field.depth({0., 10., 30.}), WithinAbs(0., Tolerance));
}
TEST_CASE("Adaptive infill field in Distance warp mode follows the distance to the surface", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field in Distance warp mode follows the distance to the surface", "[FillTpmsAdaptive]")
{
// In a cube the depth falls linearly along every ray from the center, so Distance warp matches Lobes.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const AdaptiveInfillField cube = radial_field(square, 20., AdaptiveInfillMode::DistanceWarp);
const TpmsRadialField cube = radial_field(square, 20., TpmsAdaptiveMode::DistanceWarp);
CHECK_THAT(radial(cube, {10., 10., 10.}), WithinAbs(0., Tolerance));
CHECK_THAT(radial(cube, {15., 10., 10.}), WithinAbs(0.5, Tolerance));
CHECK_THAT(radial(cube, {10., 10., 20.}), WithinAbs(1., Tolerance));
// 20 x 20 x 60 mm: Lobes grades the axis towards the top and the bottom, Distance warp keeps it deep.
const AdaptiveInfillField lobes = radial_field(square, 60.);
const AdaptiveInfillField warp = radial_field(square, 60., AdaptiveInfillMode::DistanceWarp);
const TpmsRadialField lobes = radial_field(square, 60.);
const TpmsRadialField warp = radial_field(square, 60., TpmsAdaptiveMode::DistanceWarp);
for (double z : {15., 45.}) {
CAPTURE(z);
CHECK_THAT(radial(lobes, {10., 10., z}), WithinAbs(0.5, Tolerance));
@@ -247,12 +247,12 @@ TEST_CASE("Adaptive infill field in Distance warp mode follows the distance to t
CHECK_THAT(radial(warp, {0., 10., 30.}), WithinAbs(1., Tolerance));
}
TEST_CASE("Adaptive infill stepped shells split a layer by depth from the surface inwards", "[AdaptiveInfillField]")
TEST_CASE("TPMS stepped shells split a layer by depth from the surface inwards", "[FillTpmsAdaptive]")
{
// The middle layer of a 40 mm cube, 20 mm from its center to every face, 20% at the surface to 5% inside.
const ExPolygons square{rectangle(0., 0., 40., 40.)};
const AdaptiveInfillField field = radial_field(square, 40., AdaptiveInfillMode::SteppedShells);
const std::vector<AdaptiveInfillShell> shells = make_adaptive_infill_shells(field, square.front(), 20., 0.2f, 0.05f, AdaptiveInfillGradient::Linear);
const TpmsRadialField field = radial_field(square, 40., TpmsAdaptiveMode::SteppedShells);
const std::vector<TpmsShell> shells = make_tpms_shells(field, square.front(), 20., 0.2f, 0.05f, TpmsAdaptiveGradient::Linear);
REQUIRE(shells.size() == 5);
CHECK_THAT(shells.front().density, WithinAbs(0.2, 1e-6));
CHECK_THAT(shells.back().density, WithinAbs(0.05, 1e-6));
@@ -270,11 +270,11 @@ TEST_CASE("Adaptive infill stepped shells split a layer by depth from the surfac
[&center](const ExPolygon &expolygon) { return expolygon.contains(center); }));
}
TEST_CASE("Adaptive infill field in 2D grades every section normal to the axis on its own", "[AdaptiveInfillField]")
TEST_CASE("TPMS radial field in 2D grades every section normal to the axis on its own", "[FillTpmsAdaptive]")
{
// 20 x 20 x 60 mm: every section normal to Z is 10 mm from its center to the sides, whatever its height.
const ExPolygons square{rectangle(0., 0., 20., 20.)};
const AdaptiveInfillField normal_z = radial_field(square, 60., AdaptiveInfillMode::NormalZ);
const TpmsRadialField normal_z = radial_field(square, 60., TpmsAdaptiveMode::NormalZ);
for (double z : {5., 30., 55.}) {
CAPTURE(z);
CHECK_THAT(radial(normal_z, {10., 10., z}), WithinAbs(0., Tolerance));
@@ -283,7 +283,7 @@ TEST_CASE("Adaptive infill field in 2D grades every section normal to the axis o
}
// Normal to X, the sections are 20 x 60 mm: 10 mm from the center to the sides, 30 mm to the top and the bottom.
const AdaptiveInfillField normal_x = radial_field(square, 60., AdaptiveInfillMode::NormalX);
const TpmsRadialField normal_x = radial_field(square, 60., TpmsAdaptiveMode::NormalX);
for (double x : {3., 10., 17.}) {
CAPTURE(x);
CHECK_THAT(radial(normal_x, {x, 15., 30.}), WithinAbs(0.5, Tolerance));
-72
View File
@@ -1,72 +0,0 @@
#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);
}
+770
View File
@@ -1,22 +1,48 @@
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/system/error_code.hpp>
#include <catch2/catch_all.hpp>
#include <libslic3r/Utils.hpp>
#include <libslic3r/libslic3r.h> // GCODEVIEWER_APP_KEY, SLIC3R_APP_KEY (via libslic3r_version.h)
#include <slic3r/plugin/PluginAuditManager.hpp>
#include <slic3r/plugin/PluginFsUtils.hpp>
#include <slic3r/plugin/PluginManager.hpp>
#include <slic3r/plugin/PythonInterpreter.hpp>
#include <slic3r/plugin/PythonPluginBridge.hpp>
#include <slic3r/plugin/PythonPluginInterface.hpp>
#include <slic3r/Utils/IPrinterAgent.hpp>
#include <slic3r/Utils/OrcaCloudServiceAgent.hpp> // secret_constants::USER_SECRET_FILENAME
#include <catch2/catch_message.hpp>
#include <catch2/catch_test_macros.hpp>
#include <catch2/matchers/catch_matchers.hpp>
#include <catch2/matchers/catch_matchers_string.hpp>
#include "libslic3r_version.h"
#include "plugin_test_utils.hpp"
#include "test_utils.hpp"
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <pybind11/cast.h>
#include <pybind11/embed.h>
#include <pybind11/eval.h>
#include <pybind11/gil.h>
#include <pybind11/pybind11.h>
#include <pybind11/pytypes.h>
#include <pybind11/stl.h>
#include <fstream>
#include <functional>
#include <ios>
#include <memory>
#include <string>
#include <utility>
#include <vector>
using namespace Slic3r;
namespace fs = boost::filesystem;
namespace py = pybind11;
namespace {
@@ -349,3 +375,747 @@ TEST_CASE("Plugin audit a scoped root can also be registered read-only", "[audit
CHECK_FALSE(mgr.check_open((readonly_dir / "vendor.json").string(), "w").allowed);
}
}
// ---------------------------------------------------------------------------
// The audit hook, run in the embedded interpreter
// ---------------------------------------------------------------------------
//
// Each TEST_CASE runs in its own process (catch_discover_tests), so each brings the plugin system
// up with the audit hook installed, once, and shuts it down at the end. The tests loop over their
// cases instead of using SECTIONs, which would restart the interpreter for every section.
namespace {
// Discovered, never loaded: the hook only needs the plugin's root and its sidecar.
const char* const AUDIT_PLUGIN_SOURCE = R"PY(# /// script
# requires-python = ">=3.12"
#
# [tool.orcaslicer.plugin]
# name = "Audit Plugin"
# description = "Plugin audit hook fixture"
# author = "OrcaSlicer"
# version = "1.0"
# type = "script"
# ///
)PY";
// A third-party library, installed outside the stdlib and outside the plugin.
const char* const LIBRARY_SOURCE = R"PY(
import socket
import urllib.request
def read_all(paths):
for path in paths:
with open(path) as f:
f.read()
def connect_from_host(port):
socket.gethostname()
socket.create_connection(("127.0.0.1", port)).close()
def fetch(url):
with urllib.request.urlopen(url) as response:
return response.read()
def fetch_each(urls):
for url in urls:
yield fetch(url)
class ReadingHandler(urllib.request.BaseHandler):
"""Reads a file while each response is processed."""
def __init__(self, path):
self.path = path
self.outcome = None
def http_response(self, request, response):
try:
open(self.path).close()
self.outcome = "ok"
except PermissionError:
self.outcome = "denied"
return response
)PY";
// Test-side Python, run outside any plugin context: files outside every allowed root, a local
// HTTP server, the library, and every module the tests use imported up front, so that no import
// happens inside a plugin context. Code from exec() runs as "<string>", which is not stdlib, so
// attempt() and fetch() stand in for plugin code.
const char* const AUDIT_SETUP = R"PY(
import http.client, http.server, os, pathlib, shutil, socket, subprocess, sys, tempfile, threading
import urllib.request, zipfile
if os.name != "nt":
import pty
APPROVED = os.path.join(OUTSIDE, "approved.txt")
PRIVATE = os.path.join(OUTSIDE, "private.txt")
MISSING = os.path.join(OUTSIDE, "missing")
TREE = os.path.join(OUTSIDE, "tree")
TRUE_COMMAND = ["cmd", "/c", "exit 0"] if os.name == "nt" else ["true"]
for path in (APPROVED, PRIVATE):
with open(path, "w") as f:
f.write("x")
for sub in ("one", "two"):
os.makedirs(os.path.join(TREE, sub))
with open(os.path.join(TREE, sub, "f.txt"), "w") as f:
f.write("x")
LIBRARY = os.path.join(OUTSIDE, "library")
os.makedirs(LIBRARY)
with open(os.path.join(LIBRARY, "netlib.py"), "w") as f:
f.write(LIBRARY_SOURCE)
sys.path.insert(0, LIBRARY)
import netlib
class Handler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
if self.path == "/redirect":
self.send_response(302)
self.send_header("Location", URL_B)
body = b""
else:
self.send_response(200)
body = b"ok"
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def log_message(self, *args):
pass
server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler)
threading.Thread(target=server.serve_forever, daemon=True).start()
PORT = server.server_address[1]
BASE = "http://127.0.0.1:%d" % PORT
URL_A, URL_B, URL_REDIRECT = BASE + "/a", BASE + "/b", BASE + "/redirect"
urllib.request.install_opener(urllib.request.build_opener(urllib.request.ProxyHandler({})))
def fetch(url):
with urllib.request.urlopen(url) as response:
return response.read()
fetch(URL_A) # urllib imports some modules on its first request
def attempt(action):
try:
action()
return "ok"
except PermissionError:
return "denied"
def spawn_checked(argv):
parent = os.getpid()
try:
return pty.spawn(argv)
finally:
if os.getpid() != parent:
os._exit(1) # a child whose exec was refused must not go on running the tests
finalizer_outcomes = []
class TrackedRequest(urllib.request.Request):
def __del__(self):
finalizer_outcomes.append(attempt(lambda: open(PRIVATE).close()))
)PY";
struct Prompt
{
std::string plugin;
std::string event;
std::vector<std::string> targets;
};
// One line per prompt, "event: target ...", so a failed check shows every prompt.
std::string describe(const std::vector<Prompt>& prompts)
{
std::string text;
for (const Prompt& prompt : prompts) {
text += prompt.event + ":";
for (const std::string& target : prompt.targets)
text += " " + target;
text += "\n";
}
return text;
}
// The plugin system with the audit hook installed, and the permission dialog replaced by a
// recorder that answers with `approve`. Declare it first in a test, so it is destroyed last.
struct AuditHarness
{
// Before initialize(): the interpreter creates {data_dir}/python/packages and {data_dir}/log.
// data_dir() is a global allowed root from then on, so the test files live in `outside`.
ScopedDataDir data{"plugin-audit-hook"};
bool initialized = PluginManager::instance().initialize();
ScopedTemporaryDir outside{"orca-plugin-audit-outside"};
std::vector<Prompt> prompts;
std::function<bool(const Prompt&)> approve = [](const Prompt&) { return false; };
AuditHarness()
{
PluginAuditManager::set_permission_prompt_for_testing(
[this](const std::string& event, const std::vector<std::string>& targets) {
prompts.push_back({PluginAuditManager::instance().current_plugin(), event, targets});
return approve(prompts.back());
});
}
~AuditHarness()
{
PluginAuditManager::set_permission_prompt_for_testing(nullptr);
PluginManager::instance().shutdown();
PythonInterpreter::instance().shutdown();
}
// Call before taking the GIL: discovery is not Python work.
void add_plugin(const std::string& key)
{
const fs::path root = data.plugins_dir() / key;
fs::create_directories(root);
std::ofstream((root / (key + ".py")).string(), std::ios::binary) << AUDIT_PLUGIN_SOURCE;
save_permissions(key, {});
PluginManager::instance().discover_plugins(/*async=*/false, /*clear=*/true);
}
// Grants as if saved in an earlier session.
void save_permissions(const std::string& key, const PluginPermissions& permissions)
{
PluginInstallState state;
state.installed_from = "local";
state.installed_version = "1.0";
state.plugin_name = key;
state.permissions = permissions;
REQUIRE(write_install_state(data.plugins_dir() / key, state));
}
PluginPermissions saved_permissions(const std::string& key) const
{
PluginInstallState state;
read_install_state(data.plugins_dir() / key, state);
return state.permissions;
}
};
// The test-side Python globals. Construct and destroy it with the GIL held; it stops the HTTP
// server before the interpreter shuts down.
struct PythonSide
{
py::dict globals;
explicit PythonSide(const AuditHarness& harness)
{
globals["OUTSIDE"] = harness.outside.path().string();
globals["LIBRARY_SOURCE"] = LIBRARY_SOURCE;
exec(AUDIT_SETUP);
}
// A Python error fails the test here, as text: an error_already_set left to propagate would
// outlive the interpreter the harness shuts down.
void exec(const char* code)
{
std::string error;
try {
py::exec(code, globals);
} catch (py::error_already_set& raised) {
error = raised.what();
}
if (!error.empty())
FAIL(error);
}
~PythonSide()
{
try {
globals["server"].attr("shutdown")();
globals["server"].attr("server_close")();
} catch (...) {
}
}
std::string str(const char* name) const { return py::str(globals[name]).cast<std::string>(); }
std::string eval(const char* expression) const { return py::str(py::eval(expression, globals)).cast<std::string>(); }
std::vector<std::string> strings(const char* name) const { return globals[name].cast<std::vector<std::string>>(); }
// Runs `code` as `plugin_key`, as a trampoline call does: inside the caller's GIL, in an
// audit scope of its own.
void run_as(const std::string& plugin_key, const char* code)
{
ScopedPluginAuditContext audit(plugin_key);
exec(code);
}
};
PluginPermissions http_grants(std::vector<std::string> urls)
{
PluginPermissions permissions;
permissions.network_http = std::move(urls);
return permissions;
}
const char* const PLUGIN = "AuditPlugin";
} // namespace
TEST_CASE("A saved request grant covers the request's own connection in a later session", "[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);
harness.save_permissions(PLUGIN, http_grants({python.str("URL_A")}));
python.run_as(PLUGIN, "outcome = attempt(lambda: fetch(URL_A))");
CHECK(python.str("outcome") == "ok");
CHECK(describe(harness.prompts).empty());
}
TEST_CASE("A saved request grant does not cover another URL or a redirect", "[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);
harness.save_permissions(PLUGIN, http_grants({python.str("URL_A"), python.str("URL_REDIRECT")}));
python.run_as(PLUGIN, R"PY(
outcomes = [attempt(lambda: fetch(url)) for url in (URL_A, URL_B, URL_REDIRECT)]
outcomes += [attempt(lambda: netlib.fetch(url)) for url in (URL_A, URL_B)]
outcomes.append(attempt(lambda: list(netlib.fetch_each([URL_A, URL_B]))))
)PY");
// The redirect target is asked for inside the approved redirecting request, and a library's
// wrapper or generator asks for the second URL as plain code does.
CHECK(python.strings("outcomes") == std::vector<std::string>{"ok", "denied", "denied", "ok", "denied", "denied"});
const std::string url_b = "urllib.Request: " + python.str("URL_B") + "\n";
CHECK(describe(harness.prompts) == url_b + url_b + url_b + url_b);
}
TEST_CASE("Code an approved operation calls back into is judged on its own", "[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);
python.exec(R"PY(
CALLBACK_COPY = os.path.join(OUTSIDE, "callback-copy")
handler = netlib.ReadingHandler(PRIVATE)
handler_opener = urllib.request.build_opener(urllib.request.ProxyHandler({}), handler)
walk_outcomes = []
copy_outcomes = []
def on_walk_error(error):
walk_outcomes.append(attempt(lambda: open(PRIVATE).close()))
def copy_file(source, destination):
copy_outcomes.append(attempt(lambda: open(PRIVATE).close()))
)PY");
PluginPermissions permissions = http_grants({python.str("URL_A")});
permissions.fs_read = {python.str("APPROVED"), python.str("MISSING")};
permissions.fs_readwrite = {python.str("TREE"), python.str("CALLBACK_COPY")};
harness.save_permissions(PLUGIN, permissions);
// A library handler that urllib calls while the approved request runs, the plugin's own
// os.walk error callback and copytree copy function (called once per file), and a library
// loop over a saved file and an unsaved one.
python.run_as(PLUGIN, R"PY(
request_outcome = attempt(lambda: handler_opener.open(URL_A).read())
for _ in os.walk(MISSING, onerror=on_walk_error):
pass
shutil.copytree(TREE, CALLBACK_COPY, copy_function=copy_file)
loop_outcome = attempt(lambda: netlib.read_all([APPROVED, PRIVATE]))
)PY");
CHECK(python.str("request_outcome") == "ok");
CHECK(python.eval("handler.outcome") == "denied");
CHECK(python.strings("walk_outcomes") == std::vector<std::string>{"denied"});
CHECK(python.strings("copy_outcomes") == std::vector<std::string>{"denied", "denied"});
CHECK(python.str("loop_outcome") == "denied");
const std::string private_file = "open: " + python.str("PRIVATE") + "\n";
CHECK(describe(harness.prompts) == private_file + private_file + private_file + private_file + private_file);
}
TEST_CASE("A saved directory walk covers each directory it lists", "[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);
PluginPermissions permissions;
permissions.fs_read = {python.str("TREE"), python.str("APPROVED")};
harness.save_permissions(PLUGIN, permissions);
// The walk lists the tree's two subdirectories on later resumes, after the plugin's own
// audited reads in between. Abandoned walks leave frames whose generator is gone, which the
// next audited event lets go.
python.run_as(PLUGIN, R"PY(
walked = []
for root, dirs, files in os.walk(TREE):
walked.append(root)
open(APPROVED).close()
globbed = list(pathlib.Path(TREE).glob("**/*"))
abandoned = os.walk(TREE)
next(abandoned)
del abandoned
open(APPROVED).close()
for _ in pathlib.Path(TREE).glob("*"):
break
open(APPROVED).close()
)PY");
CHECK(py::len(python.globals["walked"]) == 3);
CHECK(py::len(python.globals["globbed"]) == 4);
CHECK(describe(harness.prompts).empty());
}
TEST_CASE("A freshly approved operation covers its nested events", "[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);
struct Case
{
const char* prompts;
const char* code;
};
// Each operation names targets no earlier one saved, so each is asked once. FsCreate and
// FsDelete grants are never saved, so those operations are asked every time. Only the
// expected prompts are approved, so a nested event left uncovered fails the operation.
std::vector<Case> cases = {
{"shutil.copytree", "shutil.copytree(TREE, os.path.join(OUTSIDE, 'copy'))"},
{"shutil.copyfile", "shutil.copyfile(APPROVED, os.path.join(OUTSIDE, 'copied.txt'))"},
{"shutil.move", "shutil.move(os.path.join(OUTSIDE, 'copied.txt'), os.path.join(OUTSIDE, 'moved.txt'))"},
{"shutil.copystat", "shutil.copystat(os.path.join(TREE, 'one', 'f.txt'), PRIVATE)"},
{"shutil.make_archive", "shutil.make_archive(os.path.join(OUTSIDE, 'archive'), 'zip', root_dir=TREE)"},
{"shutil.unpack_archive", "shutil.unpack_archive(os.path.join(OUTSIDE, 'archive.zip'), os.path.join(OUTSIDE, 'unpacked'))"},
{"pathlib.Path.glob", "list(pathlib.Path(TREE).glob('**/*'))"},
{"subprocess.Popen", "subprocess.run(TRUE_COMMAND, check=True)"},
{"tempfile.mkdtemp", "tempfile.mkdtemp(dir=OUTSIDE)"},
{"tempfile.mkstemp", "os.close(tempfile.mkstemp(dir=OUTSIDE)[0])"},
{"shutil.rmtree", "shutil.rmtree(os.path.join(OUTSIDE, 'copy'))"},
// send() runs after connect() has returned, so its own grant is asked once.
{"http.client.connect\nhttp.client.send",
"connection = http.client.HTTPConnection('127.0.0.1', PORT)\n"
"connection.request('GET', '/a')\n"
"connection.getresponse().read()\n"
"connection.close()"},
};
#ifndef _WIN32
cases.push_back({"os.fwalk", "list(os.fwalk(os.path.join(OUTSIDE, 'unpacked')))"});
// The forked child's exec is covered by the spawn too: had it prompted, the child's own
// denial would end it with status 1.
cases.push_back({"pty.spawn", "spawn_status = spawn_checked(TRUE_COMMAND + ['spawned'])"});
#endif
for (const Case& operation : cases) {
INFO(operation.code);
const std::string expected = std::string(operation.prompts) + "\n";
harness.approve = [&expected](const Prompt& prompt) {
return expected.find(prompt.event + "\n") != std::string::npos;
};
harness.prompts.clear();
python.run_as(PLUGIN, operation.code);
std::string asked;
for (const Prompt& prompt : harness.prompts)
asked += (asked.empty() ? "" : "\n") + prompt.event;
CHECK(asked == operation.prompts);
}
#ifndef _WIN32
CHECK(python.eval("spawn_status") == "0");
#endif
}
TEST_CASE("An event with no target is granted by name and remembered", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
harness.approve = [](const Prompt& prompt) { return prompt.event != "socket.sendto"; };
py::gil_scoped_acquire gil;
PythonSide python(harness);
python.run_as(PLUGIN, R"PY(
socket.socket().close()
udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sendto_outcome = attempt(lambda: udp.sendto(b"x", ("127.0.0.1", 9)))
udp.close()
)PY");
// The socket is asked for once, without a target to show; the datagram's address is a target.
CHECK(python.str("sendto_outcome") == "denied");
CHECK(describe(harness.prompts) == "socket.__new__:\nsocket.sendto: ('127.0.0.1', 9)\n");
CHECK(harness.saved_permissions(PLUGIN).events == std::vector<std::string>{"Socket:socket.__new__"});
CHECK(harness.saved_permissions(PLUGIN).network_socket.empty());
harness.prompts.clear();
python.run_as(PLUGIN, "socket.socket().close()");
CHECK(describe(harness.prompts).empty());
// A grant for an event with no target covers nothing else: a library's approved
// gethostname() does not carry the connection it makes next.
harness.approve = [](const Prompt& prompt) { return prompt.event == "socket.gethostname"; };
python.run_as(PLUGIN, "connect_outcome = attempt(lambda: netlib.connect_from_host(PORT))");
CHECK(python.str("connect_outcome") == "denied");
CHECK(describe(harness.prompts) == "socket.gethostname:\nsocket.getaddrinfo: 127.0.0.1\n");
harness.prompts.clear();
harness.approve = [](const Prompt&) { return true; };
#ifndef _WIN32
// sendmsg() without an address names no target either.
python.run_as(PLUGIN, R"PY(
left, right = socket.socketpair()
left.sendmsg([b"x"])
left.close()
right.close()
)PY");
CHECK(describe(harness.prompts) == "socket.sendmsg:\n");
CHECK(harness.saved_permissions(PLUGIN).events ==
std::vector<std::string>{"Socket:socket.__new__", "Socket:socket.gethostname", "Socket:socket.sendmsg"});
#endif
}
TEST_CASE("An event grant does not stand in for another access or a target of the same name", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin(PLUGIN);
harness.approve = [asked = 0](const Prompt&) mutable { return asked++ == 0; };
py::gil_scoped_acquire gil;
PythonSide python(harness);
python.exec(R"PY(
READ_FD = os.open(APPROVED, os.O_RDONLY)
WRITE_FD = os.open(os.path.join(OUTSIDE, "scratch.txt"), os.O_WRONLY | os.O_CREAT)
)PY");
// A file descriptor has no path to show or save: a granted read of one is not a write.
python.run_as(PLUGIN, R"PY(
fd_outcomes = [attempt(lambda: open(READ_FD, "r", closefd=False).close()),
attempt(lambda: open(WRITE_FD, "w", closefd=False).close())]
)PY");
CHECK(python.strings("fd_outcomes") == std::vector<std::string>{"ok", "denied"});
CHECK(describe(harness.prompts) == "open:\nopen:\n");
CHECK(harness.saved_permissions(PLUGIN).events == std::vector<std::string>{"FsRead:open"});
// A saved os.fork event grant is not an os.system command named "os.fork".
PluginPermissions permissions;
permissions.events = {"FsRead:open", "ProcessCreate:os.fork"};
harness.save_permissions(PLUGIN, permissions);
harness.prompts.clear();
python.run_as(PLUGIN, R"PY(
reread_outcome = attempt(lambda: open(READ_FD, "r", closefd=False).close())
system_outcome = attempt(lambda: os.system("os.fork"))
os.close(READ_FD)
os.close(WRITE_FD)
)PY");
CHECK(python.str("reread_outcome") == "ok");
CHECK(python.str("system_outcome") == "denied");
REQUIRE(harness.prompts.size() == 1);
CHECK(harness.prompts.front().event == "os.system");
CHECK(harness.saved_permissions(PLUGIN).process.empty());
}
TEST_CASE("A finalizer run when a root frame is let go is judged as the plugin without a prompt", "[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);
PluginPermissions permissions = http_grants({python.str("URL_A")});
permissions.fs_read = {python.str("APPROVED")};
harness.save_permissions(PLUGIN, permissions);
PluginAuditManager& mgr = PluginAuditManager::instance();
// Once the request returns, only its held frame keeps the request object alive, and the
// request's finalizer reads a file the plugin was never granted.
python.run_as(PLUGIN, "fetch(TrackedRequest(URL_A))");
CHECK(python.strings("finalizer_outcomes") == std::vector<std::string>{"denied"}); // at scope end
CHECK(describe(harness.prompts).empty());
CHECK_FALSE(mgr.audit_denial_pending());
python.exec("finalizer_outcomes.clear()");
python.run_as(PLUGIN, R"PY(
fetch(TrackedRequest(URL_A))
open(APPROVED).close() # the next event lets the returned request's frame go
outcomes_before_scope_end = list(finalizer_outcomes)
)PY");
CHECK(python.strings("outcomes_before_scope_end") == std::vector<std::string>{"denied"});
CHECK(describe(harness.prompts).empty());
// The finalizer's denial during another event's decision leaves that event's own denial intact.
python.exec("finalizer_outcomes.clear()");
python.run_as(PLUGIN, R"PY(
fetch(TrackedRequest(URL_A))
try:
os.listdir(OUTSIDE)
outer_error = ""
except PermissionError as error:
outer_error = str(error)
)PY");
CHECK(python.strings("finalizer_outcomes") == std::vector<std::string>{"denied"});
CHECK_THAT(python.str("outer_error"), Catch::Matchers::ContainsSubstring("os.listdir"));
CHECK(describe(harness.prompts) == "os.listdir: " + python.str("OUTSIDE") + "\n");
AuditViolation violation;
REQUIRE(mgr.last_violation(violation));
CHECK(violation.event_name == "os.listdir");
CHECK(mgr.audit_denial_pending());
}
TEST_CASE("A nested audit scope does not see the outer scope's running operations", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.add_plugin("PluginA");
harness.add_plugin("PluginB");
py::gil_scoped_acquire gil;
PythonSide python(harness);
PluginPermissions permissions;
permissions.fs_read = {python.str("TREE")};
harness.save_permissions("PluginA", permissions);
{
// Two walks plugin A started and suspended after listing the tree itself.
ScopedPluginAuditContext plugin_a("PluginA");
python.exec("first = os.walk(TREE)\nnext(first)\nsecond = os.walk(TREE)\nnext(second)");
{
// Plugin B resuming one of them is asked for each subdirectory, as plugin B.
ScopedPluginAuditContext plugin_b("PluginB");
python.exec("listed_by_b = len(list(first))");
}
// Back in plugin A's scope, its walks are covered again.
python.exec("listed_by_a = len(list(second))");
}
CHECK(python.globals["listed_by_b"].cast<int>() == 0);
CHECK(python.globals["listed_by_a"].cast<int>() == 2);
REQUIRE(harness.prompts.size() == 2);
for (const Prompt& prompt : harness.prompts) {
CHECK(prompt.plugin == "PluginB");
CHECK(prompt.event == "os.scandir");
}
}
TEST_CASE("Root frames held when a capability call ends are let go with the GIL, as the plugin", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
harness.approve = [](const Prompt& prompt) { return prompt.event == "os.walk"; };
py::gil_scoped_acquire gil;
PythonSide python(harness);
(void) PythonPluginBridge::instance(); // force the embedded module registration into the binary
python.globals["orca"] = py::module_::import("orca");
// walk_and_drop() leaves a returned walk's frame held by the audit scope alone, with a marker
// in its locals; the frame is let go when the capability call ends, which runs the marker's
// finalizer.
python.exec(R"PY(
class Marker:
def __init__(self, action=None):
self.action = action
def __del__(self):
finalized.append(self.action() if self.action else "ran")
finalized = []
def walk_and_drop(marker):
walker = os.walk(TREE, onerror=marker)
next(walker)
del walker
class ConfigCapability(orca.PythonPluginBase):
def get_name(self):
return "config_cap"
def get_default_config(self):
walk_and_drop(Marker())
return {"finalized_during_call": len(finalized)}
def has_config_ui(self):
walker = os.walk(TREE, onerror=Marker(lambda: attempt(lambda: open(PRIVATE).close())))
next(walker)
attempt(lambda: os.listdir(OUTSIDE)) # denied: the trampoline clears the plugin key
del walker # the walk's frame is now held by the audit scope alone
raise RuntimeError("the capability fails after a denial")
class TicketAgent(orca.printer_agent.PrinterAgentBase):
def get_name(self):
return "ticket_agent"
def request_bind_ticket(self):
walk_and_drop(Marker())
return (0, "ticket")
)PY");
py::object config_instance = python.globals["ConfigCapability"]();
auto config_cap = config_instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
config_cap->set_audit_plugin_key("GilPlugin");
config_cap->set_resolved_identity("config_cap", PluginCapabilityType::Script);
py::object agent_instance = python.globals["TicketAgent"]();
auto agent_cap = agent_instance.cast<std::shared_ptr<PluginCapabilityInterface>>();
agent_cap->set_audit_plugin_key("GilPlugin");
agent_cap->set_resolved_identity("ticket_agent", PluginCapabilityType::PrinterConnection);
auto agent = std::dynamic_pointer_cast<IPrinterAgent>(agent_cap);
REQUIRE(agent);
// A host call arrives without the GIL; both calls take it before their audit scope.
nlohmann::json config;
int ticket_result = -1;
std::string ticket;
{
py::gil_scoped_release released;
config = config_cap->get_default_config();
ticket_result = agent->request_bind_ticket(&ticket);
}
CHECK(config == nlohmann::json{{"finalized_during_call", 0}});
CHECK(ticket_result == 0);
CHECK(ticket == "ticket");
CHECK(python.strings("finalized") == std::vector<std::string>{"ran", "ran"});
// On the trampoline's denial path the frame is let go as the plugin, though the ambient key
// was cleared: the finalizer's read is audited and denied without a prompt.
python.exec("finalized.clear()");
harness.prompts.clear();
CHECK_THROWS_AS(config_cap->has_config_ui(), py::error_already_set);
CHECK(python.strings("finalized") == std::vector<std::string>{"denied"});
CHECK(describe(harness.prompts) == "os.walk: " + python.str("TREE") + "\nos.listdir: " + python.str("OUTSIDE") + "\n");
}
TEST_CASE("Stdlib code is recognized through a symlinked Python home", "[audit][Python]")
{
AuditHarness harness;
if (!harness.initialized)
SKIP("Bundled Python interpreter unavailable: " + PythonInterpreter::instance().last_error());
py::gil_scoped_acquire gil;
const py::dict paths = py::module_::import("sysconfig").attr("get_paths")();
const fs::path stdlib = py::str(paths["stdlib"]).cast<std::string>();
const fs::path purelib = py::str(paths["purelib"]).cast<std::string>();
const fs::path link = harness.outside.path() / "linked-stdlib";
boost::system::error_code ec;
fs::create_directory_symlink(stdlib, link, ec);
if (ec)
SKIP("Cannot create a directory symlink here: " + ec.message());
CHECK(PluginAuditManager::is_stdlib_file((stdlib / "os.py").string()));
CHECK(PluginAuditManager::is_stdlib_file((link / "urllib" / "request.py").string()));
CHECK(PluginAuditManager::is_stdlib_file("<frozen os>"));
CHECK_FALSE(PluginAuditManager::is_stdlib_file((purelib / "requests" / "api.py").string()));
CHECK_FALSE(PluginAuditManager::is_stdlib_file((link / "site-packages" / "requests" / "api.py").string()));
CHECK_FALSE(PluginAuditManager::is_stdlib_file((harness.outside.path() / "plugin.py").string()));
CHECK_FALSE(PluginAuditManager::is_stdlib_file("<string>"));
}