mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-07-16 07:22:10 +00:00
Separated Infills enhancements (#14674)
Co-authored-by: Rodrigo Faselli <162915171+RF47@users.noreply.github.com>
This commit is contained in:
@@ -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<ObjectID> 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
|
||||
|
||||
|
||||
@@ -2739,13 +2739,19 @@ static void polylines_from_paths(const std::vector<MonotonicRegionLink> &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);
|
||||
|
||||
|
||||
@@ -157,6 +157,10 @@ public:
|
||||
ExPolygons lslices;
|
||||
ExPolygons lslices_extrudable; // BBS: the extrudable part of lslices used for tree support
|
||||
std::vector<BoundingBox> 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<BoundingBox> lslices_separated_component_bboxes;
|
||||
|
||||
// BBS
|
||||
ExPolygons loverhangs;
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<size_t> 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<size_t> 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<BoundingBox> 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;
|
||||
|
||||
|
||||
@@ -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<ConfigOptionEnum<InfillPattern>>("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<ConfigOptionEnum<InfillPattern>>("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"})
|
||||
|
||||
Reference in New Issue
Block a user