diff --git a/src/libslic3r/Fill/Fill.cpp b/src/libslic3r/Fill/Fill.cpp index 68180cd2f7..fbaade10ab 100644 --- a/src/libslic3r/Fill/Fill.cpp +++ b/src/libslic3r/Fill/Fill.cpp @@ -1335,7 +1335,7 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: bool is_per_model_center = is_top_or_bottom && params.center_of_surface_pattern == CenterOfSurfacePattern::Each_Model && is_centered_infill; bool is_separate_infill = !is_top_or_bottom && surface_fill.params.separated_infills && ( - is_centered_infill || + is_separable_infill_pattern(surface_fill.params.pattern) || params.config->solid_infill_rotate_template != "" || params.config->sparse_infill_rotate_template != "" ); @@ -1364,59 +1364,30 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive: // Orca: separate infill / per-model pattern centering. // - // First assign this fill region to the model part whose slice at this layer overlaps it - // the most. A strict "contains" test is ambiguous for assemblies whose parts overlap (a - // region may sit inside several parts, or straddle a boundary and be inside none), so we - // pick by intersection area instead. - // - // The center must belong to an *overlap group*, not a single part: parts that - // touch/overlap form one connected physical body that shares a single center, while a - // part detached from the rest of the assembly gets its own. This holds for both - // separated infills and Each_Model surface centering (Each_Model == per connected body). - // firstLayerObjGroups() already holds these connected components, so we widen the chosen - // part's bbox to the whole group it belongs to. + // Center the pattern on each connected body of the object independently, so every piece + // is filled exactly as if it were sliced on its own: touching/overlapping parts merge + // into one body sharing a center, while separate parts and disconnected islands (even + // interleaved-but-not-touching ones, e.g. chain links) each get their own. The body each + // island belongs to, and its full bounding box, were resolved in 3D by PrintObject:: + // infill() (lslices_separated_component_bboxes, aligned with this layer's lslices). We + // match this fill region to the island it overlaps most, then re-use the whole-object + // bounding box (origin-centered — identical extent to the default, so coverage and cost + // are unchanged) re-centered on that body. if (is_per_model_center || is_separate_infill) { - double best_overlap = 0.; - ObjectID best_vol_id; - const PrintInstance* best_instance = nullptr; - for (const auto& instance : this->object()->instances()) { - for (const auto& volume : instance.print_object->firstLayerObjSlice()) { - if (f->layer_id >= volume.slices.size()) - continue; - const double overlap = area(intersection_ex(volume.slices[f->layer_id], ExPolygons{expoly})); - if (overlap > best_overlap) { - best_overlap = overlap; - best_vol_id = volume.volume_id; - best_instance = &instance; - } + double best_overlap = 0.; + BoundingBox best_component; + for (size_t r = 0; r < this->lslices.size() && r < this->lslices_separated_component_bboxes.size(); ++ r) { + const double overlap = area(intersection_ex(this->lslices[r], expoly)); + if (overlap > best_overlap) { + best_overlap = overlap; + best_component = this->lslices_separated_component_bboxes[r]; } } - if (best_instance) { - const Transform3d matrix = best_instance->model_instance->get_matrix(); - Point shift = best_instance->shift; // get_volume_bbox takes a non-const ref - auto& volumes = best_instance->model_instance->get_object()->volumes; - - // Volume ids to center on: the whole overlap group the winning part belongs to, - // falling back to just that part if it isn't part of any group. - std::vector center_ids; - for (const auto& group : best_instance->print_object->firstLayerObjGroups()) { - bool in_group = false; - for (const ObjectID& vid : group.volume_ids) - if (vid == best_vol_id) { in_group = true; break; } - if (in_group) { center_ids = group.volume_ids; break; } - } - if (center_ids.empty()) - center_ids.push_back(best_vol_id); - - BoundingBox bbox; - for (const ObjectID& vid : center_ids) - for (auto model_volume : volumes) - if (vid.id == model_volume->id().id) { - bbox.merge(model_volume->get_volume_bbox(matrix, shift, true)); - break; - } - if (bbox.defined) - f->set_bounding_box(bbox); + if (best_component.defined) { + const Point c = best_component.center(); + BoundingBox part_bbox = bbox; // origin-centered, whole-object extent (from above) + part_bbox.translate(c.x(), c.y()); // re-center on this body + f->set_bounding_box(part_bbox); } } // - End: separate infill / per-model pattern centering diff --git a/src/libslic3r/Fill/FillRectilinear.cpp b/src/libslic3r/Fill/FillRectilinear.cpp index 5638dea869..138b50bc88 100644 --- a/src/libslic3r/Fill/FillRectilinear.cpp +++ b/src/libslic3r/Fill/FillRectilinear.cpp @@ -2739,13 +2739,19 @@ 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())); - - // 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; } bool FillRectilinear::fill_surface_by_lines(const Surface *surface, const FillParams ¶ms, float angleBase, float pattern_shift, Polylines &polylines_out) @@ -3098,8 +3104,11 @@ bool FillRectilinear::fill_surface_trapezoidal( const coord_t d2 = coord_t(0.5 * period - d1); - // Align bounding box to the grid - bb.merge(align_to_grid(bb.min, Point(period, period))); + // Align bounding box to the grid, phased through the box center so separated infills align + // each part on itself (grid_center is the origin for a standalone object / feature off). + // Captured before the merge, which grows bb and would otherwise shift its center. + const Point grid_center = bb.center(); + bb.merge(align_to_grid(bb.min, Point(period, period), grid_center)); const coord_t xmin = bb.min.x(); const coord_t xmax = bb.max.x(); const coord_t ymin = bb.min.y(); @@ -3146,11 +3155,17 @@ bool FillRectilinear::fill_surface_trapezoidal( flip_vertical = !flip_vertical; } - // transpose points for odd infill layers (taking infill combination into account) + // transpose points for odd infill layers (taking infill combination into account). + // Orca: mirror across the diagonal through grid_center (not the origin), so the swapped + // layers stay aligned with the center-phased grid. For a standalone object / feature off, + // grid_center is the origin and this is a plain x/y swap. if (infill_layer_id % 2 == 1) { for (Polyline& pl : polylines) { for (Point& p : pl.points) { - std::swap(p.x(), p.y()); + const coord_t dx = p.x() - grid_center.x(); + const coord_t dy = p.y() - grid_center.y(); + p.x() = grid_center.x() + dy; + p.y() = grid_center.y() + dx; } } } @@ -3341,6 +3356,14 @@ bool FillRectilinear::fill_surface_trapezoidal( break; } + // Orca: cases 1 & 2 build the pattern symmetrically around the origin, so on their own they + // phase to the global origin and every part shares one grid. Shift the pattern onto the box + // center this->bounding_box carries, so separated infills align each part on itself. The center + // is the origin for a standalone object (or when the feature is off), making this a no-op there. + if (Pattern_type != 0) + for (Polyline &pl : polylines) + pl.translate(rotate_vector.second); + // Apply multiline fill multiline_fill(polylines, params, spacing); diff --git a/src/libslic3r/Layer.hpp b/src/libslic3r/Layer.hpp index d871bcbea2..8a5aa78036 100644 --- a/src/libslic3r/Layer.hpp +++ b/src/libslic3r/Layer.hpp @@ -157,6 +157,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; // BBS ExPolygons loverhangs; diff --git a/src/libslic3r/Model.cpp b/src/libslic3r/Model.cpp index eb9153a661..1177a5227d 100644 --- a/src/libslic3r/Model.cpp +++ b/src/libslic3r/Model.cpp @@ -2708,23 +2708,6 @@ const TriangleMesh& ModelVolume::get_convex_hull() const return *m_convex_hull.get(); } -// Orca: get volume bbox for separate infill -static std::mutex mtx_model; -BoundingBox ModelVolume::get_volume_bbox(const Transform3d &matrix, Point &shift, bool apply_cache = false) { - std::unique_lock l(mtx_model); // locks function here - // Orca: the cache is keyed by the instance transform/shift; a ModelVolume is shared - // across instances, so returning the cache blindly would hand back another instance's bbox. - if (m_cached_volume_bbox.defined && apply_cache - && matrix.isApprox(m_cached_volume_bbox_matrix) - && shift == m_cached_volume_bbox_shift) - return m_cached_volume_bbox; - auto hull = get_convex_hull_2d(matrix); - hull.translate(-shift); - m_cached_volume_bbox_matrix = matrix; - m_cached_volume_bbox_shift = shift; - return m_cached_volume_bbox = hull.bounding_box().polygon().bounding_box(); -} - //BBS: refine the model part names ModelVolumeType ModelVolume::type_from_string(const std::string &s) { diff --git a/src/libslic3r/Model.hpp b/src/libslic3r/Model.hpp index 518fbbaa1c..2d46bc4cdf 100644 --- a/src/libslic3r/Model.hpp +++ b/src/libslic3r/Model.hpp @@ -923,7 +923,6 @@ public: //Orca: cache clearing procedure to ensure that the shape is positioned accurately when manipulating it void clear_cache() { m_cached_trans_matrix = Transform3d::Identity().inverse(); // get unvelivable matrix - m_cached_volume_bbox.reset(); m_convex_hull_2d.clear(); m_cached_2d_polygon.clear(); }; @@ -969,9 +968,6 @@ public: // Get count of errors in the mesh int get_repaired_errors_count() const; - BoundingBox get_volume_bbox(const Transform3d &matrix, Point &shift, bool apply_cache); - void reset_volume_bbox() { m_cached_volume_bbox.reset(); }; - // Helpers for loading / storing into AMF / 3MF files. static ModelVolumeType type_from_string(const std::string &s); static std::string type_to_string(const ModelVolumeType t); @@ -1059,9 +1055,6 @@ private: mutable Transform3d m_cached_trans_matrix; //BBS, used for convex_hell_2d acceleration mutable Polygon m_cached_2d_polygon; //BBS, used for convex_hell_2d acceleration Geometry::Transformation m_transformation; - mutable BoundingBox m_cached_volume_bbox; //Orca: used for separated infills - mutable Transform3d m_cached_volume_bbox_matrix{Transform3d::Identity()}; //Orca: cache key for m_cached_volume_bbox - mutable Point m_cached_volume_bbox_shift{Point(0, 0)}; //Orca: cache key for m_cached_volume_bbox //BBS: add convex_hell_2d related logic void calculate_convex_hull_2d(const Geometry::Transformation &transformation) const; diff --git a/src/libslic3r/Print.cpp b/src/libslic3r/Print.cpp index 10d7b46349..1242d5a7ff 100644 --- a/src/libslic3r/Print.cpp +++ b/src/libslic3r/Print.cpp @@ -209,7 +209,9 @@ bool Print::invalidate_state_by_config_options(const ConfigOptionResolver & /* n "chamber_minimal_temperature", "thumbnails", "thumbnails_format", - "anisotropic_surfaces", "center_of_surface_pattern", "separated_infills", + "anisotropic_surfaces", + "center_of_surface_pattern", + "separated_infills", "seam_gap", "role_based_wipe_speed", "wipe_speed", diff --git a/src/libslic3r/PrintConfig.cpp b/src/libslic3r/PrintConfig.cpp index 5a8771881f..b48fd806c5 100644 --- a/src/libslic3r/PrintConfig.cpp +++ b/src/libslic3r/PrintConfig.cpp @@ -6889,13 +6889,12 @@ void PrintConfigDef::init_fff_params() def = this->add("separated_infills", coBool); def->label = L("Separated infills"); def->category = L("Strength"); - def->tooltip = L("Aligns the internal infill pattern of each part independently instead of across the whole object or assembly.\n" - "By default, aligned infill patterns share a single origin for the entire object, so the pattern of every " - "part is referenced to the same point. When enabled, each connected body is aligned on its own: parts that " - "touch or overlap are treated as one body and share an origin, while parts detached from the rest each get " - "their own.\n Useful when an assembly groups several distinct objects that should each keep a self-centered infill.\n" - "Only affects centered infill patterns (Archimedean Chords, Octagram Spiral) and patterns driven by an " - "infill rotation template."); + def->tooltip = L("Centers the internal infill of each part on itself, as if it were sliced on its own, instead of on the " + "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."); def->mode = comExpert; def->set_default_value(new ConfigOptionBool(false)); diff --git a/src/libslic3r/PrintConfig.hpp b/src/libslic3r/PrintConfig.hpp index 10d4b50226..10b03fea51 100644 --- a/src/libslic3r/PrintConfig.hpp +++ b/src/libslic3r/PrintConfig.hpp @@ -110,6 +110,34 @@ 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. +inline bool is_separable_infill_pattern(InfillPattern pattern) +{ + switch (pattern) { + case ipRectilinear: + case ipAlignedRectilinear: + case ipZigZag: + case ipCrossZag: + case ipLockedZag: + case ipGrid: + case ipTriangles: + case ipStars: // tri-hexagon + case ipCubic: + case ipQuarterCubic: + case ipLateralHoneycomb: + case ipLateralLattice: + case ipHilbertCurve: + case ipArchimedeanChords: + case ipOctagramSpiral: + return true; + default: + return false; + } +} + enum class IroningType { NoIroning, TopSurfaces, diff --git a/src/libslic3r/PrintObject.cpp b/src/libslic3r/PrintObject.cpp index 9d829b2ccf..d89a9b8ab4 100644 --- a/src/libslic3r/PrintObject.cpp +++ b/src/libslic3r/PrintObject.cpp @@ -563,11 +563,6 @@ void PrintObject::prepare_infill() { if (! this->set_started(posPrepareInfill)) return; - - // Orca: clear all volume bbox caches - for (auto volume : this->model_object()->volumes) - volume->reset_volume_bbox(); - m_print->set_status(25, L("Generating infill regions")); if (m_typed_slices) { // To improve robustness of detect_surfaces_type() when reslicing (working with typed slices), see GH issue #7442. @@ -710,6 +705,72 @@ void PrintObject::infill() if (this->set_started(posInfill)) { m_print->set_status(35, L("Generating infill toolpath")); + + // Orca: precompute the object's 3D connected bodies for separated infills / per-model + // centering. Two islands belong to the same body when their slices overlap on adjacent + // layers; islands that only overlap in top-down projection but never touch (e.g. interleaved + // chain links) stay separate, matching "split to objects". Each layer island then records + // the full bounding box of its body, so its infill is centered on that body as if it were + // sliced alone. Done once here, before the parallel fill, and only when a region needs it. + 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) { + needs_separated_components = true; + break; + } + } + // Fast path: the feature only changes anything when the object is made of more than one + // connected body. Detect that cheaply the same way as "Split to objects" — more than one + // model part, or a single part whose mesh is splittable (is_splittable() is cached). A single + // body already shares the object center, i.e. the default, so skip the connectivity pass. + if (needs_separated_components) { + int parts = 0; + const ModelVolume *first_part = nullptr; + for (const ModelVolume *v : this->model_object()->volumes) + if (v->is_model_part()) { ++ parts; first_part = v; } + if (parts <= 1 && ! (first_part != nullptr && first_part->is_splittable())) + needs_separated_components = false; + } + for (Layer *layer : m_layers) + layer->lslices_separated_component_bboxes.clear(); + if (needs_separated_components) { + const size_t nl = m_layers.size(); + std::vector offset(nl + 1, 0); // flat index of the first island of each layer + for (size_t i = 0; i < nl; ++ i) + offset[i + 1] = offset[i] + m_layers[i]->lslices.size(); + const size_t nreg = offset[nl]; + // Union-find over every (layer, island). + std::vector parent(nreg); + for (size_t i = 0; i < nreg; ++ i) parent[i] = i; + auto find = [&parent](size_t x) { + while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } + return x; + }; + auto unite = [&](size_t a, size_t b) { a = find(a); b = find(b); if (a != b) parent[a] = b; }; + // Join islands that overlap between two consecutive layers. + for (size_t i = 0; i + 1 < nl; ++ i) { + const Layer *la = m_layers[i], *lb = m_layers[i + 1]; + for (size_t a = 0; a < la->lslices.size(); ++ a) + for (size_t b = 0; b < lb->lslices.size(); ++ b) + if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) && + ! intersection_ex(la->lslices[a], lb->lslices[b]).empty()) + unite(offset[i] + a, offset[i + 1] + b); + } + // 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]); + // Store the body bbox for every island. + 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)]; + } + } + const auto& adaptive_fill_octree = this->m_adaptive_fill_octrees.first; const auto& support_fill_octree = this->m_adaptive_fill_octrees.second; diff --git a/src/slic3r/GUI/ConfigManipulation.cpp b/src/slic3r/GUI/ConfigManipulation.cpp index edae8c6c79..4bd9749073 100644 --- a/src/slic3r/GUI/ConfigManipulation.cpp +++ b/src/slic3r/GUI/ConfigManipulation.cpp @@ -734,10 +734,10 @@ void ConfigManipulation::toggle_print_fff_options(DynamicPrintConfig *config, in toggle_line("anisotropic_surfaces", has_centered_surface); // Orca: separate infills - bool is_internal_infill_centered = is_centered_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_centered); + 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); // Orca: no need gaps for (auto el : {"gap_fill_target", "filter_out_gap_fill"})