diff --git a/docs/HLSD/separated-infills.md b/docs/HLSD/separated-infills.md new file mode 100644 index 0000000000..b7f12f4199 --- /dev/null +++ b/docs/HLSD/separated-infills.md @@ -0,0 +1,90 @@ +# Separated infills — High Level Design + +## Purpose and scope + +An object's infill patterns are laid out from one reference point, the center +of the object. When an object groups several parts that do not touch, every +part cuts the same object-wide pattern at a different place, so equal parts get +different infill. `separated_infills` lays the infill of every connected body +out from the center of that body instead, as if the body were sliced on its own. + +The option covers sparse infill, internal solid infill and bridges. Top and +bottom surfaces are left to `center_of_surface_pattern`, which centers the +Archimedean Chords and Octagram Spiral surface patterns. The option is off by +default; with it off, or for an object made of a single body, no fill changes. +Adaptive Cubic and Support Cubic do not depend on the option: they always fill +each body on its own (see Octree infill). + +## Bodies + +`PrintObject::prepare_infill()` groups the islands of every layer (`lslices`) +into 3D connected bodies before bridges are detected, so bridge anchors and +printed infill share one origin. Islands on adjacent layers belong to one body +when their slices overlap. Parts that touch or overlap form one body. Separate +parts, disconnected islands of one mesh, and interleaved parts that never touch, +such as chain links, each form their own. Every island stores the index of its +body in `Layer::lslices_separated_component_ids`, and +`PrintObject::separated_body_bboxes()` holds the bounding box of each body over +all its layers. + +The pass runs when a region uses separated infills, per-model surface centering +or an octree infill pattern. It is skipped when the object has one model part +that cannot be split, since a single body already shares the object center. + +## Centering a fill + +`infill_body()` matches each fill region to the island it overlaps most, among +the islands whose bounding boxes overlap it, and the filler takes the bounding +box of that island's body instead of the object's. The box covers every layer +of the body, which is the box the body gets when sliced alone, so patterns that +depend on its extent as well as its center come out the same too. Bridge +anchoring (`Layer::generate_sparse_infill_polylines_for_anchoring()`) makes the +same choice, so the anchors match the printed infill. + +The patterns follow the body's box in one of two ways: + +- Rectilinear and its variants, Line, Grid, Triangles, Tri-hexagon, Cubic, + Quarter Cubic, Lateral Lattice, Lateral Honeycomb and the plane-path patterns + (Hilbert Curve, Archimedean Chords, Octagram Spiral) are laid out from the + box: they phase their lines through its center, and Hilbert Curve and the Zig + Zag links start from its corner. `Fill::extended_object_bounding_box()` + extends the box about its center, so it also serves a box that is not + centered on the origin. +- Honeycomb, 3D Honeycomb, Cross Hatch, Gyroid, TPMS-D and TPMS-FK are laid out + from the coordinate origin, which is the object center. They return true from + `Fill::aligned_to_origin()`, and `Fill::fill_surface()` moves each region so + that the box center lands on the origin, fills it, and moves the paths back. + With the default box the center is the origin, so nothing moves. + +`is_separable_infill_pattern()` lists these patterns. The settings show the +option only when the sparse infill pattern is one of them. + +## Octree infill + +Adaptive Cubic and Support Cubic take their lines from an octree, laid out from +the center of the mesh it is built from and refined near its surfaces. An +octree of the whole object would lay every part out from the object's center +and refine it near the other parts, so these patterns +(`is_octree_infill_pattern()`) always fill each body on its own, and the +settings hide the option for them. + +For an object of several bodies, `PrintObject::prepare_adaptive_infill_data()` +builds one octree per body (`FillAdaptive::Octrees`) from the triangles of that +body only, which is the octree the body gets when sliced alone. Each connected +component of the mesh goes to the body that most of a few sampled triangles lie +on. A sample is taken a layer height inside the solid, behind the triangle, and +looked up in the islands of the nearest layer. Each internal bridge surface goes +to the body of its island. The fill takes the octree of the region's body, from +the same `infill_body()`. The octree of the whole object is built only for an +object of a single body, or when some body received no triangles, which then +uses it. + +## Patterns left out + +Lightning grows its trees over the whole object, so moving a reference point +cannot center it on one body. Concentric and Spiral Inset follow the outline of +each region and need no centering. + +Solid infill at full density spaces its lines over the extent of each region, +so it is already independent of the other bodies. Only bridges, which keep +their line spacing, and the plane-path solid patterns depend on the center. diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index b1d8ff2b13..3dadb20253 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -29,6 +29,7 @@ #include "ExtrusionEntity.hpp" #include "Fill.hpp" #include "libslic3r/Fill/FillBase.hpp" +#include "FillAdaptive.hpp" #include "FillRectilinear.hpp" #include "FillLightning.hpp" #include "FillConcentricInternal.hpp" @@ -926,7 +927,6 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p params.lateral_lattice_angle_2 = region_config.lateral_lattice_angle_2; params.infill_overhang_angle = region_config.infill_overhang_angle; params.center_of_surface_pattern = region_config.center_of_surface_pattern; - params.separated_infills = region_config.separated_infills; if (params.pattern == ipLockedZag) { params.infill_lock_depth = scale_(region_config.infill_lock_depth); params.skin_infill_depth = scale_(region_config.skin_infill_depth); @@ -999,6 +999,9 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p // (which would unnecessarily split fill batching). // Stored on SurfaceFillParams; copied to FillParams during conversion. params.gyroid_optimized = (params.pattern == ipGyroid) && region_config.gyroid_optimized; + // Orca: Likewise separated_infills only where it can move the pattern. + params.separated_infills = region_config.separated_infills && is_separable_infill_pattern(params.pattern) && + params.extrusion_role != erTopSolidInfill && params.extrusion_role != erBottomSurface; if (params.extrusion_role == erInternalInfill) { params.angle = calculate_infill_rotation_angle(layer.object(), layer.id(), region_config.infill_direction.value, @@ -1271,29 +1274,28 @@ std::vector group_fills(const Layer &layer, LockRegionParam &lock_p // Orca: Anchors and printed infill must share the same body origin. Keep the choice // here so per-model surface centering and separated sparse infill cannot drift apart. -static BoundingBox infill_bounding_box(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly, BoundingBox bbox) +// Returns the connected body the fill region is laid out on, or -1 to keep the object's origin. +static int infill_body(const Layer &layer, const SurfaceFill &fill, const ExPolygon &expoly) { const auto ¶ms = fill.params; - const auto &config = layer.regions()[fill.region_id]->region().config(); - const bool external = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface; - const bool per_model = external && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && + const bool per_model = (params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface) && + params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && (params.pattern == ipArchimedeanChords || params.pattern == ipOctagramSpiral); - const bool separate = !external && params.separated_infills && - (is_separable_infill_pattern(params.pattern) || !config.solid_infill_rotate_template.value.empty() || - !config.sparse_infill_rotate_template.value.empty()); - if (per_model || separate) { - double best_overlap = 0.; - for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_bboxes.size(); ++i) { + int body = -1; + if (per_model || params.separated_infills || is_octree_infill_pattern(params.pattern)) { + const BoundingBox box = get_extents(expoly); + double best_overlap = 0.; + for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_ids.size(); ++i) { + if (! layer.lslices_bboxes[i].overlap(box)) + continue; const double overlap = area(intersection_ex(layer.lslices[i], expoly)); if (overlap > best_overlap) { best_overlap = overlap; - const Point center = layer.lslices_separated_component_bboxes[i].center(); - bbox = layer.object()->bounding_box(); - bbox.translate(center.x(), center.y()); + body = int(layer.lslices_separated_component_ids[i]); } } } - return bbox; + return body; } #ifdef SLIC3R_DEBUG_SLICE_PROCESSING @@ -1318,7 +1320,7 @@ void export_group_fills_to_svg(const char *path, const std::vector #endif // friend to Layer -void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator) +void Layer::make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) { for (LayerRegion *layerm : m_regions) layerm->fills.clear(); @@ -1351,7 +1353,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: f->z = this->print_z; f->angle = surface_fill.params.angle; f->fixed_angle = surface_fill.params.fixed_angle; - f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree; + const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees; f->print_config = &this->object()->print()->config(); f->print_object_config = &this->object()->config(); if (surface_fill.params.pattern == ipConcentricInternal) { @@ -1443,8 +1445,10 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: params.can_reverse = false; for (ExPolygon& expoly : surface_fill.expolygons) { - // Orca: Reuse the body origin used for bridge anchoring, resetting it for each surface. - f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox)); + // Orca: Reuse the body box and octree used for bridge anchoring, resetting them for each surface. + const int body = infill_body(*this, surface_fill, expoly); + f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox); + f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr; f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes); if (params.symmetric_infill_y_axis) { @@ -1512,7 +1516,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: * - For lightning/adaptive patterns, the respective generators are wired so their * polylines match the final infill layout. */ -Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator) const +Polylines Layer::generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator) const { LockRegionParam skin_inner_param; std::vector surface_fills = group_fills(*this, skin_inner_param); @@ -1570,7 +1574,7 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc f->z = this->print_z; f->angle = surface_fill.params.angle; f->fixed_angle = surface_fill.params.fixed_angle; - f->adapt_fill_octree = (surface_fill.params.pattern == ipSupportCubic) ? support_fill_octree : adaptive_fill_octree; + const FillAdaptive::Octrees *octrees = surface_fill.params.pattern == ipSupportCubic ? support_fill_octrees : adaptive_fill_octrees; f->print_config = &this->object()->print()->config(); f->print_object_config = &this->object()->config(); @@ -1617,8 +1621,10 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc params.extrusion_role = surface_fill.params.extrusion_role; for (ExPolygon &expoly : surface_fill.expolygons) { - // Orca: Match the per-body origin of make_fills() before generating physical anchors. - f->set_bounding_box(infill_bounding_box(*this, surface_fill, expoly, bbox)); + // Orca: Match the per-body box and octree of make_fills() before generating physical anchors. + const int body = infill_body(*this, surface_fill, expoly); + f->set_bounding_box(body >= 0 ? this->object()->separated_body_bboxes()[body] : bbox); + f->adapt_fill_octree = octrees ? octrees->get(body) : nullptr; // Spacing is modified by the filler to indicate adjustments. Reset it for each expolygon. f->spacing = surface_fill.params.spacing; surface_fill.surface.expolygon = std::move(expoly); diff --git a/src/libslic3r/Fill/Fill3DHoneycomb.hpp b/src/libslic3r/Fill/Fill3DHoneycomb.hpp index a31608310d..66e16510da 100644 --- a/src/libslic3r/Fill/Fill3DHoneycomb.hpp +++ b/src/libslic3r/Fill/Fill3DHoneycomb.hpp @@ -25,6 +25,7 @@ public: // pattern is placed on top of previous layers bool use_bridge_flow() const override { return false; } bool is_self_crossing() override { return false; } + bool aligned_to_origin() const override { return true; } protected: void _fill_surface_single( diff --git a/src/libslic3r/Fill/FillAdaptive.hpp b/src/libslic3r/Fill/FillAdaptive.hpp index 9b424133a2..57cbe713f1 100644 --- a/src/libslic3r/Fill/FillAdaptive.hpp +++ b/src/libslic3r/Fill/FillAdaptive.hpp @@ -14,6 +14,7 @@ #include "libslic3r/BoundingBox.hpp" #include "libslic3r/ExPolygon.hpp" #include "FillBase.hpp" +#include #include #include #include @@ -37,6 +38,27 @@ struct Octree; struct OctreeDeleter { void operator()(Octree *p); }; using OctreePtr = std::unique_ptr; +// Orca: One octree per body (see Layer::lslices_separated_component_ids), and one of the whole object +// for objects of a single body or with a body that has none of its own. +struct Octrees +{ + OctreePtr object; + std::vector bodies; + + // A body without an octree, or body -1, uses the object's, or any body's when the object has none. + Octree *get(int body) const + { + if (body >= 0 && size_t(body) < bodies.size() && bodies[body]) + return bodies[body].get(); + if (object) + return object.get(); + for (const OctreePtr &octree : bodies) + if (octree) + return octree.get(); + return nullptr; + } +}; + // Calculate line spacing for // 1) adaptive cubic infill // 2) adaptive internal support cubic infill diff --git a/src/libslic3r/Fill/FillBase.cpp b/src/libslic3r/Fill/FillBase.cpp index 95b7dc28db..a852f2c42b 100644 --- a/src/libslic3r/Fill/FillBase.cpp +++ b/src/libslic3r/Fill/FillBase.cpp @@ -128,6 +128,9 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms) { // Perform offset. Slic3r::ExPolygons expp = offset_ex(surface->expolygon, float(scale_(this->overlap - 0.5 * this->spacing))); + // Orca: Separated infills move the box center onto each body; origin-aligned patterns follow it. + const Point shift = this->aligned_to_origin() && ! empty(this->bounding_box) ? this->bounding_box.center() : Point::Zero(); + translate(expp, -shift); // Create the infills for each of the regions. Polylines polylines_out; for (size_t i = 0; i < expp.size(); ++ i) @@ -137,6 +140,8 @@ Polylines Fill::fill_surface(const Surface *surface, const FillParams ¶ms) _infill_direction(surface), std::move(expp[i]), polylines_out); + for (Polyline &pl : polylines_out) + pl.translate(shift); return polylines_out; } @@ -1591,13 +1596,18 @@ BoundaryInfillGraph create_boundary_infill_graph(const Polylines &infill_ordered // The extended bounding box of the whole object that covers any rotation of every layer. BoundingBox Fill::extended_object_bounding_box() const { - BoundingBox out = bounding_box; + // Orca: Extend about the box center, which separated infills move off the origin. + const Point c = this->bounding_box.center(); + BoundingBox out = this->bounding_box; + out.translate(-c.x(), -c.y()); out.merge(Point(out.min.y(), out.min.x())); out.merge(Point(out.max.y(), out.max.x())); // The bounding box is scaled by sqrt(2.) to ensure that the bounding box // covers any possible rotations. - return out.scaled(sqrt(2.)); + out = out.scaled(sqrt(2.)); + out.translate(c.x(), c.y()); + return out; } void Fill::connect_infill(Polylines &&infill_ordered, const std::vector &boundary_src, const BoundingBox &bbox, Polylines &polylines_out, const double spacing, const FillParams ¶ms) diff --git a/src/libslic3r/Fill/FillBase.hpp b/src/libslic3r/Fill/FillBase.hpp index 7599b2a0d2..45dcfc87e8 100644 --- a/src/libslic3r/Fill/FillBase.hpp +++ b/src/libslic3r/Fill/FillBase.hpp @@ -188,6 +188,9 @@ public: // Return true if infill has a consistent pattern between layers. virtual bool has_consistent_pattern() const { return false; } + // Orca: Is the pattern laid out from the origin instead of the bounding box center? + virtual bool aligned_to_origin() const { return false; } + // Perform the fill. virtual Polylines fill_surface(const Surface *surface, const FillParams ¶ms); virtual ThickPolylines fill_surface_arachne(const Surface* surface, const FillParams& params); diff --git a/src/libslic3r/Fill/FillCrossHatch.hpp b/src/libslic3r/Fill/FillCrossHatch.hpp index 4b18443aae..14eb7fcbf4 100644 --- a/src/libslic3r/Fill/FillCrossHatch.hpp +++ b/src/libslic3r/Fill/FillCrossHatch.hpp @@ -19,6 +19,7 @@ public: Fill *clone() const override { return new FillCrossHatch(*this); }; ~FillCrossHatch() override {} bool is_self_crossing() override { return false; } + bool aligned_to_origin() const override { return true; } protected: void _fill_surface_single( diff --git a/src/libslic3r/Fill/FillGyroid.hpp b/src/libslic3r/Fill/FillGyroid.hpp index ad032d5904..a60216824b 100644 --- a/src/libslic3r/Fill/FillGyroid.hpp +++ b/src/libslic3r/Fill/FillGyroid.hpp @@ -20,6 +20,7 @@ public: // require bridge flow since most of this pattern hangs in air bool use_bridge_flow() const override { return false; } bool is_self_crossing() override { return false; } + bool aligned_to_origin() const override { return true; } // Correction applied to regular infill angle to maximize printing // speed in default configuration (degrees) diff --git a/src/libslic3r/Fill/FillHoneycomb.hpp b/src/libslic3r/Fill/FillHoneycomb.hpp index fd1fe039de..d682e69b2c 100644 --- a/src/libslic3r/Fill/FillHoneycomb.hpp +++ b/src/libslic3r/Fill/FillHoneycomb.hpp @@ -20,6 +20,7 @@ class FillHoneycomb : public Fill public: ~FillHoneycomb() override {} bool is_self_crossing() override { return false; } + bool aligned_to_origin() const override { return true; } protected: Fill* clone() const override { return new FillHoneycomb(*this); }; diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 85de82e27c..f7954ef7fc 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -2750,23 +2750,6 @@ static void polylines_from_paths(const std::vector &path, c } } -// The extended bounding box of the whole object that covers any rotation of every layer. -BoundingBox FillRectilinear::extended_object_bounding_box() const { - // Build the extension around the box center. The transpose merge and the sqrt(2.) scaling - // (which covers any possible rotation) are both defined about the origin, so a box that is not - // origin-centered — e.g. a separated-infill box re-centered on a single assembly part — would be - // distorted. Shift to the origin first and back afterwards; for the default origin-centered box - // the two translations cancel and this is identical to the original behavior. - const Point c = this->bounding_box.center(); - BoundingBox out = this->bounding_box; - out.translate(-c.x(), -c.y()); - out.merge(Point(out.min.y(), out.min.x())); - out.merge(Point(out.max.y(), out.max.x())); - out = out.scaled(sqrt(2.)); - out.translate(c.x(), c.y()); - return out; -} - bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillParams ¶ms, float angleBase, float pattern_shift, Polylines &polylines_out) { // At the end, only the new polylines will be rotated back. @@ -2801,7 +2784,13 @@ bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillPa // For infill that needs to be consistent between layers (like Zig Zag), // we use bounding box of whole object to match vertical lines between layers. BoundingBox bounding_box_src = poly_with_offset.bounding_box_src(); - BoundingBox bounding_box = this->has_consistent_pattern() ? this->extended_object_bounding_box() : bounding_box_src; + BoundingBox bounding_box = bounding_box_src; + if (this->has_consistent_pattern()) { + // Orca: The polygons are rotated about the origin, so follow the box center to where it was rotated. + const Point c = this->bounding_box.center(); + bounding_box = this->extended_object_bounding_box(); + bounding_box.translate(c.rotated(- rotate_vector.first) - c); + } // define flow spacing according to requested density if (params.full_infill() && !params.dont_adjust) { diff --git a/src/libslic3r/Fill/FillRectilinear.hpp b/src/libslic3r/Fill/FillRectilinear.hpp index 59bb56a5c8..56f4505bd2 100644 --- a/src/libslic3r/Fill/FillRectilinear.hpp +++ b/src/libslic3r/Fill/FillRectilinear.hpp @@ -42,9 +42,6 @@ protected: }; bool fill_surface_by_multilines(const Surface *surface, FillParams params, const std::initializer_list &sweep_params, Polylines &polylines_out); bool fill_surface_trapezoidal(const Surface *surface, FillParams params, Polylines &polylines_out, int Pattern_type); - - // The extended bounding box of the whole object that covers any rotation of every layer. - BoundingBox extended_object_bounding_box() const; }; class FillAlignedRectilinear : public FillRectilinear diff --git a/src/libslic3r/Fill/FillTpmsD.hpp b/src/libslic3r/Fill/FillTpmsD.hpp index 8dfa12196a..ce041c98fe 100644 --- a/src/libslic3r/Fill/FillTpmsD.hpp +++ b/src/libslic3r/Fill/FillTpmsD.hpp @@ -31,6 +31,7 @@ public: Polylines& polylines_out) override; bool is_self_crossing() override { return false; } + bool aligned_to_origin() const override { return true; } // Density adjustment to have a good %of weight. static constexpr double DensityAdjust = 2.1; diff --git a/src/libslic3r/Fill/FillTpmsFK.hpp b/src/libslic3r/Fill/FillTpmsFK.hpp index d1fec0e93f..e9b6b89913 100644 --- a/src/libslic3r/Fill/FillTpmsFK.hpp +++ b/src/libslic3r/Fill/FillTpmsFK.hpp @@ -31,6 +31,7 @@ public: Polylines& polylines_out) override; bool is_self_crossing() override { return false; } + bool aligned_to_origin() const override { return true; } }; diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index 3bc25a0363..74df3e370c 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -33,7 +33,7 @@ class PrintObject; class Print; namespace FillAdaptive { - struct Octree; + struct Octrees; }; namespace FillLightning { @@ -170,10 +170,10 @@ public: ExPolygons lslices; ExPolygons lslices_extrudable; // BBS: the extrudable part of lslices used for tree support std::vector lslices_bboxes; - // Orca: for separated infills / per-model centering. Aligned with lslices: for each island, the - // full bounding box of the 3D connected body (across all layers) it belongs to. Populated by - // PrintObject::infill() only when the feature is used; empty otherwise. - std::vector lslices_separated_component_bboxes; + // Orca: for separated infills / per-model centering / octree infills. Aligned with lslices: for each + // island, the 3D connected body (across all layers) it belongs to, indexing + // PrintObject::separated_body_bboxes(). Populated by PrintObject::prepare_infill() only when needed. + std::vector lslices_separated_component_ids; // BBS ExPolygons loverhangs; @@ -208,9 +208,9 @@ public: void make_perimeters(); // Phony version of make_fills() without parameters for Perl integration only. void make_fills() { this->make_fills(nullptr, nullptr); } - void make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive::Octree* support_fill_octree, FillLightning::Generator* lightning_generator = nullptr); - Polylines generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Octree *adaptive_fill_octree, - FillAdaptive::Octree *support_fill_octree, + void make_fills(const FillAdaptive::Octrees* adaptive_fill_octrees, const FillAdaptive::Octrees* support_fill_octrees, FillLightning::Generator* lightning_generator = nullptr); + Polylines generate_sparse_infill_polylines_for_anchoring(const FillAdaptive::Octrees *adaptive_fill_octrees, + const FillAdaptive::Octrees *support_fill_octrees, FillLightning::Generator* lightning_generator) const; void make_ironing(); // Returns the filament id (1-based) the region is ironed with, or -1 when the diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index 75c4b65ed1..6cca66e44b 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -1696,6 +1696,9 @@ indexed_triangle_set ModelObject::raw_indexed_triangle_set() const size_t j = out.indices.size(); append(out.vertices, v->mesh().its.vertices); append(out.indices, v->mesh().its.indices); + // Orca: Point the volume's triangles at its own vertices, which follow those of the volumes before it. + for (size_t k = j; k < out.indices.size(); ++ k) + out.indices[k] += stl_triangle_vertex_indices::Constant(int(i)); const Transform3d& m = v->get_matrix(); for (; i < out.vertices.size(); ++ i) out.vertices[i] = (m * out.vertices[i].cast()).cast().eval(); diff --git a/src/libslic3r/Print.hpp b/src/libslic3r/Print.hpp index b4b64a0cd2..25d08316b6 100644 --- a/src/libslic3r/Print.hpp +++ b/src/libslic3r/Print.hpp @@ -375,6 +375,8 @@ public: Transform3d trafo_centered() const { Transform3d t = this->trafo(); t.pretranslate(Vec3d(- unscale(m_center_offset.x()), - unscale(m_center_offset.y()), 0)); return t; } const PrintInstances& instances() const { return m_instances; } + // Orca: Bounding box of each connected body, indexed by Layer::lslices_separated_component_ids. + const std::vector& separated_body_bboxes() const { return m_separated_body_bboxes; } PrintInstances &instances() { return m_instances; } // Whoever will get a non-const pointer to PrintObject will be able to modify its layers. @@ -581,8 +583,8 @@ private: void discover_horizontal_shells(); void combine_infill(); void _generate_support_material(); - std::pair prepare_adaptive_infill_data( - const std::vector>& surfaces_w_bottom_z) const; + std::pair prepare_adaptive_infill_data( + const std::vector>& surfaces_w_layer) const; FillLightning::GeneratorPtr prepare_lightning_infill_data(); // BBS @@ -614,7 +616,8 @@ private: // so that next call to make_perimeters() performs a union() before computing loops bool m_typed_slices = false; - std::pair m_adaptive_fill_octrees; + std::pair m_adaptive_fill_octrees; + std::vector m_separated_body_bboxes; FillLightning::GeneratorPtr m_lightning_generator; std::vector < VolumeSlices > firstLayerObjSliceByVolume; diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 774bc28762..f76b8c35f9 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -7611,8 +7611,8 @@ void PrintConfigDef::init_fff_params() "whole assembly. Parts that touch or overlap are treated as one body and share a center; separate parts " "(or distinct 3D objects) each get their own.\n" "Useful when an assembly groups several objects that should each keep a consistent, self-centered infill.\n" - "Affects line and grid patterns and rotation-template infills.\n" - "Patterns locked to global coordinates (Gyroid, Honeycomb, TPMS, ...) are unaffected."); + "Adaptive Cubic and Support Cubic always center each part on itself, and Lightning infill is generated for " + "the whole object and is unaffected."); def->mode = comExpert; def->set_default_value(new ConfigOptionBool(false)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index a7affb50b4..43c3358cc9 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -136,25 +136,31 @@ enum InfillPattern : int { ipCount, }; -// Orca: Infill patterns whose alignment origin follows the fill bounding box, so the -// "separated_infills" option can re-center them per connected body. Patterns evaluated in -// absolute/global coordinates (Gyroid, TPMS, Honeycomb, CrossHatch, ...) or that are shape-relative -// (Concentric) ignore that bounding box and are therefore excluded. +// Orca: Infill patterns that the "separated_infills" option can center on each connected body. inline bool is_separable_infill_pattern(InfillPattern pattern) { switch (pattern) { + case ipMonotonic: + case ipMonotonicLine: case ipRectilinear: case ipAlignedRectilinear: case ipZigZag: case ipCrossZag: case ipLockedZag: + case ipLine: case ipGrid: case ipTriangles: case ipStars: // tri-hexagon case ipCubic: case ipQuarterCubic: + case ipHoneycomb: + case ip3DHoneycomb: case ipLateralHoneycomb: case ipLateralLattice: + case ipCrossHatch: + case ipTpmsD: + case ipTpmsFK: + case ipGyroid: case ipHilbertCurve: case ipArchimedeanChords: case ipOctagramSpiral: @@ -164,6 +170,9 @@ 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 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 // than one line per infill wall; a single line makes them plain crossing lines with nothing to round. diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 12b17fa2ed..e65314a68a 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -67,6 +67,7 @@ #include #include +#include #include #include @@ -719,7 +720,8 @@ void PrintObject::prepare_infill() bool needs_separated_components = false; for (size_t i = 0; i < this->num_printing_regions(); ++ i) { const PrintRegionConfig &rc = this->printing_region(i).config(); - if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model) { + if (rc.separated_infills || rc.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model || + (rc.sparse_infill_density > 0 && is_octree_infill_pattern(rc.sparse_infill_pattern))) { needs_separated_components = true; break; } @@ -736,8 +738,9 @@ void PrintObject::prepare_infill() if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable())) needs_separated_components = false; } + m_separated_body_bboxes.clear(); for (Layer *layer : m_layers) - layer->lslices_separated_component_bboxes.clear(); + layer->lslices_separated_component_ids.clear(); if (needs_separated_components) { const size_t nl = m_layers.size(); std::vector offset(nl + 1, 0); // Orca: flat index of the first island of each layer @@ -788,17 +791,20 @@ void PrintObject::prepare_infill() }); } } - // Orca: Full bounding box of each body, indexed by its union-find root. - std::vector body_bbox(nreg); - for (size_t i = 0; i < nl; ++ i) - for (size_t a = 0; a < m_layers[i]->lslices.size(); ++ a) - body_bbox[find(offset[i] + a)].merge(m_layers[i]->lslices_bboxes[a]); - // Orca: Store the body bbox for every island. + // Orca: Number the bodies by their first island and merge the bounding boxes of their islands. + std::vector body_of_root(nreg, size_t(-1)); for (size_t i = 0; i < nl; ++ i) { Layer *layer = m_layers[i]; - layer->lslices_separated_component_bboxes.resize(layer->lslices.size()); - for (size_t a = 0; a < layer->lslices.size(); ++ a) - layer->lslices_separated_component_bboxes[a] = body_bbox[find(offset[i] + a)]; + layer->lslices_separated_component_ids.resize(layer->lslices.size()); + for (size_t a = 0; a < layer->lslices.size(); ++ a) { + size_t &body = body_of_root[find(offset[i] + a)]; + if (body == size_t(-1)) { + body = m_separated_body_bboxes.size(); + m_separated_body_bboxes.emplace_back(); + } + m_separated_body_bboxes[body].merge(layer->lslices_bboxes[a]); + layer->lslices_separated_component_ids[a] = body; + } } } @@ -836,16 +842,13 @@ void PrintObject::infill() if (this->set_started(posInfill)) { m_print->set_status(35, L("Generating infill toolpath")); - const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first; - const auto& support_fill_octree = this->m_adaptive_fill_octrees.second; - BOOST_LOG_TRIVIAL(debug) << "Filling layers in parallel - start"; tbb::parallel_for( tbb::blocked_range(0, m_layers.size()), - [this, &adaptive_fill_octree = adaptive_fill_octree, &support_fill_octree = support_fill_octree](const tbb::blocked_range& range) { + [this](const tbb::blocked_range& range) { for (size_t layer_idx = range.begin(); layer_idx < range.end(); ++ layer_idx) { m_print->throw_if_canceled(); - m_layers[layer_idx]->make_fills(adaptive_fill_octree.get(), support_fill_octree.get(), this->m_lightning_generator.get()); + m_layers[layer_idx]->make_fills(&m_adaptive_fill_octrees.first, &m_adaptive_fill_octrees.second, this->m_lightning_generator.get()); } } ); @@ -1110,14 +1113,69 @@ void PrintObject::simplify_extrusion_path() } } -std::pair PrintObject::prepare_adaptive_infill_data( - const std::vector> &surfaces_w_bottom_z) const +// Orca: Separated body of the island containing a point of a layer, else of the island outline nearest within 1 mm, or -1. +static int separated_body_at(const Layer &layer, const Point &point) +{ + int body = -1; + double best = scaled(1.); + for (size_t i = 0; i < layer.lslices.size() && i < layer.lslices_separated_component_ids.size() && best > 0.; ++ i) { + BoundingBox bbox = layer.lslices_bboxes[i]; + bbox.offset(coord_t(best)); + if (! bbox.contains(point)) + continue; + const double dist = layer.lslices[i].contains(point) ? 0. : (layer.lslices[i].point_projection(point) - point).cast().norm(); + if (dist < best) { + best = dist; + body = int(layer.lslices_separated_component_ids[i]); + } + } + return body; +} + +// Orca: The object mesh in the octree frame split by separated body. Each connected component goes to the body +// most of its sampled triangles lie on, sampled a layer height inside the solid at the layer nearest to them. +static std::vector split_mesh_by_body(const PrintObject &object, const indexed_triangle_set &mesh, size_t num_bodies) +{ + const Eigen::Matrix3d to_object = FillAdaptive::transform_to_world().toRotationMatrix(); + const double inset = object.config().layer_height.value; + std::vector bodies(num_bodies); + for (const indexed_triangle_set &component : its_split(mesh)) { + std::vector votes(num_bodies, 0); + const size_t step = std::max(1, component.indices.size() / 8); + for (size_t i = 0; i < component.indices.size(); i += step) { + const stl_triangle_vertex_indices &tri = component.indices[i]; + const Vec3d a = component.vertices[tri[0]].cast(), b = component.vertices[tri[1]].cast(), + d = component.vertices[tri[2]].cast(); + const Vec3d normal = (b - a).cross(d - a); + const double area2 = normal.norm(); + const Vec3d c = to_object * ((a + b + d) / 3. - (area2 > 0. ? Vec3d(normal * (inset / area2)) : Vec3d::Zero())); + size_t lo = 0, hi = object.layer_count(); + while (lo < hi) { + const size_t mid = (lo + hi) / 2; + if (object.get_layer(int(mid))->slice_z < c.z()) + lo = mid + 1; + else + hi = mid; + } + if (lo == object.layer_count() || (lo > 0 && c.z() - object.get_layer(int(lo) - 1)->slice_z < object.get_layer(int(lo))->slice_z - c.z())) + -- lo; + if (const int body = separated_body_at(*object.get_layer(int(lo)), Point(scaled(c.x()), scaled(c.y()))); body >= 0) + ++ votes[body]; + } + if (const auto best = std::max_element(votes.begin(), votes.end()); *best > 0) + its_merge(bodies[best - votes.begin()], component); + } + return bodies; +} + +std::pair PrintObject::prepare_adaptive_infill_data( + const std::vector> &surfaces_w_layer) const { using namespace FillAdaptive; auto [adaptive_line_spacing, support_line_spacing] = adaptive_fill_line_spacing(*this); if ((adaptive_line_spacing == 0. && support_line_spacing == 0.) || this->layers().empty()) - return std::make_pair(OctreePtr(), OctreePtr()); + return {}; indexed_triangle_set mesh = this->model_object()->raw_indexed_triangle_set(); // Rotate mesh and build octree on it with axis-aligned (standart base) cubes. @@ -1125,27 +1183,60 @@ std::pair PrintObject::prepare its_transform(mesh, to_octree * this->trafo_centered(), true); // Triangulate internal bridging surfaces. - std::vector> overhangs(std::max(surfaces_w_bottom_z.size(), size_t(1))); + std::vector> overhangs(std::max(surfaces_w_layer.size(), size_t(1))); // ^ make sure vector is not empty, even with no briding surfaces we still want to build the adaptive trees later, some continue normally - tbb::parallel_for(tbb::blocked_range(0, surfaces_w_bottom_z.size()), - [this, &to_octree, &overhangs, &surfaces_w_bottom_z](const tbb::blocked_range &range) { + tbb::parallel_for(tbb::blocked_range(0, surfaces_w_layer.size()), + [this, &to_octree, &overhangs, &surfaces_w_layer](const tbb::blocked_range &range) { PRINT_OBJECT_TIME_LIMIT_MILLIS(PRINT_OBJECT_TIME_LIMIT_DEFAULT); for (int surface_idx = range.begin(); surface_idx < range.end(); ++surface_idx) { std::vector &out = overhangs[surface_idx]; m_print->throw_if_canceled(); - append(out, triangulate_expolygon_3d(surfaces_w_bottom_z[surface_idx].first->expolygon, - surfaces_w_bottom_z[surface_idx].second)); + append(out, triangulate_expolygon_3d(surfaces_w_layer[surface_idx].first->expolygon, + float(surfaces_w_layer[surface_idx].second->bottom_z()))); for (Vec3d &p : out) p = (to_octree * p).eval(); } }); + + // Orca: Each body gets the octree it has when sliced on its own, from its own triangles. + std::pair octrees; + const size_t num_bodies = m_separated_body_bboxes.size(); + bool need_object = num_bodies <= 1; + if (num_bodies > 1) { + const std::vector body_meshes = split_mesh_by_body(*this, mesh, num_bodies); + need_object = std::any_of(body_meshes.begin(), body_meshes.end(), [](const indexed_triangle_set &its) { return its.indices.empty(); }); + std::vector> body_overhangs(num_bodies); + for (size_t i = 0; i < surfaces_w_layer.size(); ++ i) + if (const int body = separated_body_at(*surfaces_w_layer[i].second, surfaces_w_layer[i].first->expolygon.contour.points.front()); body >= 0) + append(body_overhangs[body], overhangs[i]); + if (adaptive_line_spacing) + octrees.first.bodies.resize(num_bodies); + if (support_line_spacing) + octrees.second.bodies.resize(num_bodies); + tbb::parallel_for(tbb::blocked_range(0, num_bodies), [&, adaptive_spacing = adaptive_line_spacing, support_spacing = support_line_spacing]( + const tbb::blocked_range &range) { + for (size_t body = range.begin(); body < range.end(); ++ body) { + m_print->throw_if_canceled(); + if (body_meshes[body].indices.empty()) + continue; + if (adaptive_spacing) + octrees.first.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], adaptive_spacing, false); + if (support_spacing) + octrees.second.bodies[body] = build_octree(body_meshes[body], body_overhangs[body], support_spacing, true); + } + }); + } + // and gather them. for (size_t i = 1; i < overhangs.size(); ++ i) append(overhangs.front(), std::move(overhangs[i])); - return std::make_pair( - adaptive_line_spacing ? build_octree(mesh, overhangs.front(), adaptive_line_spacing, false) : OctreePtr(), - support_line_spacing ? build_octree(mesh, overhangs.front(), support_line_spacing, true) : OctreePtr()); + // Orca: The object's octree only serves bodies that have none of their own. + if (need_object && adaptive_line_spacing) + octrees.first.object = build_octree(mesh, overhangs.front(), adaptive_line_spacing, false); + if (need_object && support_line_spacing) + octrees.second.object = build_octree(mesh, overhangs.front(), support_line_spacing, true); + return octrees; } FillLightning::GeneratorPtr PrintObject::prepare_lightning_infill_data() @@ -2963,14 +3054,14 @@ void PrintObject::bridge_over_infill() std::map infill_lines; // SECTION to generate infill polylines { - std::vector> surfaces_w_bottom_z; + std::vector> surfaces_w_layer; for (const auto &pair : surfaces_by_layer) { for (const CandidateSurface &c : pair.second) { - surfaces_w_bottom_z.emplace_back(c.original_surface, c.region->m_layer->bottom_z()); + surfaces_w_layer.emplace_back(c.original_surface, c.region->m_layer); } } - this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_bottom_z); + this->m_adaptive_fill_octrees = this->prepare_adaptive_infill_data(surfaces_w_layer); std::vector layers_to_generate_infill; for (const auto &pair : surfaces_by_layer) { @@ -2986,8 +3077,8 @@ void PrintObject::bridge_over_infill() for (size_t job_idx = r.begin(); job_idx < r.end(); job_idx++) { size_t lidx = layers_to_generate_infill[job_idx]; infill_lines.at( - lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(po->m_adaptive_fill_octrees.first.get(), - po->m_adaptive_fill_octrees.second.get(), + lidx) = po->get_layer(lidx)->generate_sparse_infill_polylines_for_anchoring(&po->m_adaptive_fill_octrees.first, + &po->m_adaptive_fill_octrees.second, po->m_lightning_generator.get()); } }); diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index 62ae955ab6..8ca79e6b64 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -871,10 +871,7 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("center_of_surface_pattern", has_centered_surface); // Orca: separate infills - bool is_internal_infill_separable = is_separable_infill_pattern(config->option>("sparse_infill_pattern")->value) || - config->opt_string("sparse_infill_rotate_template") != "" || - config->opt_string("solid_infill_rotate_template") != ""; - toggle_line("separated_infills", is_internal_infill_separable); + toggle_line("separated_infills", is_separable_infill_pattern(pattern)); // Fill order is only meaningful for the center-based surface fill patterns; hide it otherwise. auto is_centered_fill = [](InfillPattern p) { return p == ipConcentric || p == ipSpiralInset || p == ipArchimedeanChords || p == ipOctagramSpiral; }; diff --git a/tests/fff_print/test_fill.cpp b/tests/fff_print/test_fill.cpp index 7247503824..19ad1711b8 100644 --- a/tests/fff_print/test_fill.cpp +++ b/tests/fff_print/test_fill.cpp @@ -1,6 +1,7 @@ #include #include +#include #include #include #include @@ -1790,3 +1791,200 @@ TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][Internal // would hide anchors that no longer coincide with printed lines. CHECK(unscale(max_distance) <= config.opt_float("resolution")); } + +// Orca: Slices the meshes as the parts of one object, where they are. +static Print &slice_parts(Print &print, DynamicPrintConfig config, const std::vector &parts) +{ + config.set_deserialize_strict({{"layer_height", 0.2}, + {"initial_layer_print_height", 0.2}, + {"elefant_foot_compensation", 0}, + {"top_shell_thickness", 0}, + {"bottom_shell_thickness", 0}}); + Model model; + Slic3r::Test::init_print({parts.front()}, print, model, config, nullptr, false); + for (size_t i = 1; i < parts.size(); ++ i) + model.objects.front()->add_volume(TriangleMesh(parts[i]), ModelVolumeType::MODEL_PART, false); + print.apply(model, config); + print.process(); + return print; +} + +// Orca: Two identical cubes in one mesh that never touch, so each is a body of its own. +static Print &slice_two_bodies(Print &print, const DynamicPrintConfig &config, double height) +{ + TriangleMesh mesh = make_cube(20, 20, height); + TriangleMesh second = make_cube(20, 20, height); + second.translate(33, 7, 0); + mesh.merge(second); + return slice_parts(print, config, {mesh}); +} + +// Orca: Counts the points sampled along both sets that the other set does not repeat. +static void count_unmatched(const Polylines &a, const Polylines &b, size_t &sampled, size_t &unmatched) +{ + const std::array sets{&a, &b}; + for (size_t i = 0; i < 2; ++ i) { + const Polylines &other = *sets[1 - i]; + const AABBTreeLines::LinesDistancer distancer(to_lines(other)); + for (const Polyline &path : *sets[i]) + for (const Point &point : path.equally_spaced_points(scale_(0.2))) { + ++ sampled; + unmatched += other.empty() || distancer.distance_from_lines(point) > scale_(0.05); + } + } +} + +static Polylines layer_paths(const Layer &layer, ExtrusionRole role) +{ + Polylines polylines; + for (const LayerRegion *region : layer.regions()) + for (const ExtrusionEntity *entity : region->fills.flatten().entities) + if (entity->role() == role) + entity->collect_polylines(polylines); + return polylines; +} + +// Orca: Share of the paths of a role that the other body does not repeat around its own center. +static double unmatched_between_bodies(const Print &print, ExtrusionRole role) +{ + size_t sampled = 0, unmatched = 0; + for (const Layer *layer : print.objects().front()->layers()) { + REQUIRE(layer->lslices.size() == 2); + const Polylines polylines = layer_paths(*layer, role); + std::array paths; + for (size_t body = 0; body < 2; ++ body) { + // Orca: Exclude the links along the walls, which each body may chain differently. + paths[body] = intersection_pl(polylines, shrink(to_polygons(layer->lslices[body]), scale_(3.))); + for (Polyline &path : paths[body]) + path.translate(-layer->lslices_bboxes[body].center()); + } + count_unmatched(paths[0], paths[1], sampled, unmatched); + } + REQUIRE(sampled > 0); + return double(unmatched) / double(sampled); +} + +// Orca: Share of the paths of a role inside a bed region that two slices of the same body do not share. +static double unmatched_between_prints(const Print &a, const Print &b, ExtrusionRole role, const Polygons ®ion) +{ + const PrintObject &object_a = *a.objects().front(), &object_b = *b.objects().front(); + REQUIRE(object_a.layer_count() == object_b.layer_count()); + size_t sampled = 0, unmatched = 0; + for (size_t i = 0; i < object_a.layer_count(); ++ i) { + std::array paths; + for (const PrintObject *object : {&object_a, &object_b}) { + Polylines &out = paths[object == &object_b]; + out = layer_paths(*object->get_layer(int(i)), role); + for (Polyline &path : out) + path.translate(object->instances().front().shift); + out = intersection_pl(out, region); + } + count_unmatched(paths[0], paths[1], sampled, unmatched); + } + REQUIRE(sampled > 0); + return double(unmatched) / double(sampled); +} + +TEST_CASE("Separated infill centers the sparse infill of each body on itself", "[Fill][Regression]") +{ + const std::string pattern = GENERATE("line", "zigzag", "crosszag", "honeycomb", "3dhoneycomb", "crosshatch", "tpmsd", "tpmsfk", "gyroid"); + const bool separated = GENERATE(false, true); + CAPTURE(pattern, separated); + auto config = DynamicPrintConfig::full_print_config(); + // Orca: The Zig Zag patterns mirror each body about its own center. + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "20%"}, + {"symmetric_infill_y_axis", true}, + {"top_shell_layers", 0}, + {"bottom_shell_layers", 0}, + {"separated_infills", separated}}); + Print print; + const double unmatched = unmatched_between_bodies(slice_two_bodies(print, config, 2.), erInternalInfill); + // Orca: Without separation both bodies cut one object-wide pattern at different places. + if (separated) + CHECK(unmatched < 0.02); + else + CHECK(unmatched > 0.5); +} + +TEST_CASE("Separated infill centers monotonic and rectilinear bridges on each body", "[Fill][InternalBridge][Regression]") +{ + // Orca: Bridges use the Monotonic pattern below monotonic top surfaces and Rectilinear otherwise. + const std::string top_pattern = GENERATE("monotonicline", "rectilinear"); + const bool separated = GENERATE(false, true); + CAPTURE(top_pattern, separated); + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", "rectilinear"}, + {"sparse_infill_density", "15%"}, + {"top_surface_pattern", top_pattern}, + {"top_shell_layers", 4}, + {"bottom_shell_layers", 0}, + {"separated_infills", separated}}); + Print print; + const double unmatched = unmatched_between_bodies(slice_two_bodies(print, config, 4.), erInternalBridgeInfill); + if (separated) + CHECK(unmatched < 0.02); + else + CHECK(unmatched > 0.5); +} + +// Orca: Share of the infill of an off center pillar, and of the frame of four overlapping bars around it, +// that each body sliced alone does not repeat. The frame is one body of several parts that holds the pillar. +static std::pair frame_and_pillar_unmatched(const DynamicPrintConfig &config) +{ + auto box = [](double x, double y, double size_x, double size_y) { + TriangleMesh mesh = make_cube(size_x, size_y, 6); + mesh.translate(x, y, 0); + return mesh; + }; + const std::vector frame{box(0, 0, 60, 14), box(0, 46, 60, 14), box(0, 0, 14, 60), box(46, 0, 14, 60)}; + const std::vector pillar{box(18, 20, 16, 16)}; + std::vector both = frame; + both.push_back(pillar.front()); + Print print_both, print_frame, print_pillar; + slice_parts(print_both, config, both); + slice_parts(print_frame, config, frame); + slice_parts(print_pillar, config, pillar); + + // Orca: Bed regions 3 mm inside the walls, away from the links along them. + auto rect = [](double x0, double y0, double x1, double y1) { + return Polygon({Point::new_scale(x0, y0), Point::new_scale(x1, y0), Point::new_scale(x1, y1), Point::new_scale(x0, y1)}); + }; + return {unmatched_between_prints(print_both, print_pillar, erInternalInfill, {rect(21, 23, 31, 33)}), + unmatched_between_prints(print_both, print_frame, erInternalInfill, diff(Polygons{rect(3, 3, 57, 57)}, Polygons{rect(11, 11, 49, 49)}))}; +} + +TEST_CASE("Separated infill fills each body like the body sliced alone", "[Fill][Regression]") +{ + // Orca: Hilbert Curve and the Zig Zag links follow the extent of the box, not only its center. + const std::string pattern = GENERATE("hilbertcurve", "zigzag", "crosszag", "gyroid"); + CAPTURE(pattern); + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "20%"}, + {"symmetric_infill_y_axis", true}, + {"top_shell_layers", 0}, + {"bottom_shell_layers", 0}, + {"separated_infills", true}}); + const std::pair unmatched = frame_and_pillar_unmatched(config); + CHECK(unmatched.first < 0.02); + CHECK(unmatched.second < 0.02); +} + +TEST_CASE("Adaptive infill fills each body like the body sliced alone", "[Fill][Regression]") +{ + const std::string pattern = GENERATE("adaptivecubic", "supportcubic"); + // Orca: Octree infill centers each body whether or not separated infills are enabled. + const bool separated = GENERATE(false, true); + CAPTURE(pattern, separated); + auto config = DynamicPrintConfig::full_print_config(); + config.set_deserialize_strict({{"sparse_infill_pattern", pattern}, + {"sparse_infill_density", "40%"}, + {"top_shell_layers", 0}, + {"bottom_shell_layers", 0}, + {"separated_infills", separated}}); + // Orca: The octree of the whole object is laid out from its center, which the off center pillar does not share. + const std::pair unmatched = frame_and_pillar_unmatched(config); + CHECK(unmatched.first < 0.02); + CHECK(unmatched.second < 0.02); +} diff --git a/tests/fff_print/test_printobject.cpp b/tests/fff_print/test_printobject.cpp index fb79abd419..dc2d5a1596 100644 --- a/tests/fff_print/test_printobject.cpp +++ b/tests/fff_print/test_printobject.cpp @@ -512,12 +512,13 @@ TEST_CASE("Separated infill keeps fragmented and nested bodies independent", "[P print.process(); const PrintObject &object = *print.objects().front(); REQUIRE(object.layer_count() > 1); + CHECK(object.separated_body_bboxes().size() == grid_size * grid_size + 2); for (const Layer *layer : object.layers()) { REQUIRE(layer->lslices.size() == grid_size * grid_size + 2); - REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size()); + REQUIRE(layer->lslices_separated_component_ids.size() == layer->lslices.size()); size_t holes = 0; for (size_t i = 0; i < layer->lslices.size(); ++ i) { - const BoundingBox &body = layer->lslices_separated_component_bboxes[i]; + const BoundingBox &body = object.separated_body_bboxes()[layer->lslices_separated_component_ids[i]]; const BoundingBox &island = layer->lslices_bboxes[i]; CHECK(body.min == island.min); CHECK(body.max == island.max); @@ -574,6 +575,7 @@ TEST_CASE("Body centering survives islands merging and splitting between layers" REQUIRE(object.get_layer(1)->lslices.size() == 3); REQUIRE(object.get_layer(2)->lslices.size() == 3); REQUIRE(object.get_layer(4)->lslices.size() == 5); + CHECK(object.separated_body_bboxes().size() == 2); BoundingBox isolated_bbox = object.get_layer(0)->lslices_bboxes.front(); for (const BoundingBox &bbox : object.get_layer(0)->lslices_bboxes) @@ -585,10 +587,10 @@ TEST_CASE("Body centering survives islands merging and splitting between layers" if (bbox.min.x() < isolated_bbox.min.x()) connected_bbox.merge(bbox); for (const Layer *layer : object.layers()) { - REQUIRE(layer->lslices_separated_component_bboxes.size() == layer->lslices.size()); + REQUIRE(layer->lslices_separated_component_ids.size() == layer->lslices.size()); for (size_t i = 0; i < layer->lslices.size(); ++ i) { const BoundingBox &expected = layer->lslices_bboxes[i].min.x() < isolated_bbox.min.x() ? connected_bbox : isolated_bbox; - const BoundingBox &actual = layer->lslices_separated_component_bboxes[i]; + const BoundingBox &actual = object.separated_body_bboxes()[layer->lslices_separated_component_ids[i]]; CHECK(actual.min == expected.min); CHECK(actual.max == expected.max); } diff --git a/tests/libslic3r/test_model.cpp b/tests/libslic3r/test_model.cpp index 626c0c0925..f62c0c43bc 100644 --- a/tests/libslic3r/test_model.cpp +++ b/tests/libslic3r/test_model.cpp @@ -1,10 +1,15 @@ #include +#include +#include +#include #include "libslic3r/TriangleMesh.hpp" #include "libslic3r/Polygon.hpp" #include "libslic3r/BoundingBox.hpp" #include "libslic3r/Point.hpp" #include +#include +#include #include "libslic3r/Model.hpp" #include "libslic3r/Geometry.hpp" @@ -44,3 +49,25 @@ TEST_CASE("A part's 2D convex hull is its footprint projected onto the bed", "[M CHECK(bb.max.y() == scaled(45.)); } } + +TEST_CASE("An object's raw mesh keeps the triangles of each part on its own vertices", "[Model]") +{ + Model model; + ModelObject *object = model.add_object(); + object->add_volume(make_cube(10, 10, 10), ModelVolumeType::MODEL_PART, false); + TriangleMesh second = make_cube(10, 10, 10); + second.translate(30, 0, 0); + object->add_volume(std::move(second), ModelVolumeType::MODEL_PART, false); + + // Two separate cubes stay two closed components, one around each cube. + const std::vector parts = its_split(object->raw_indexed_triangle_set()); + REQUIRE(parts.size() == 2); + std::vector min_x; + for (const indexed_triangle_set &part : parts) { + CHECK(part.indices.size() == 12); + min_x.push_back(bounding_box(part).min.x()); + } + std::sort(min_x.begin(), min_x.end()); + CHECK_THAT(min_x.front(), Catch::Matchers::WithinAbs(0., 1e-4)); + CHECK_THAT(min_x.back(), Catch::Matchers::WithinAbs(30., 1e-4)); +}