mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-11 11:07:40 +00:00
Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill (#15206)
* Fix internal bridges over Hilbert Curve/Octagram Spiral sparse infill For patterns with curved/turning anchor lines (Hilbert Curve, Octagram Spiral), the bridge_over_infill algorithm produced incorrect results: 1. determine_bridging_angle: sampling curved anchor orientations produced noise across all turning directions (0/90/180/270°) instead of a single dominant one, yielding unstable bridge angles with 180° spread. Fix: use the configured infill_direction + 90° directly, bypassing the noisy sampling. The old blind +0.25*PI (Hilbert) and +1/16*PI (Octagram) offsets are removed. 2. construct_anchored_polygon: curved Hilbert/Octagram anchors intersected each vertical scan line many times at wildly different Y positions, producing chaotic polygon sections — holes in random places, bridges over air, rotated bridges. Fix: replace the curved infill polylines with synthetic straight lines parallel to infill_direction, spaced at the real infill line spacing (flow_spacing / density). Lines are centered on the limiting_area bbox center so that after rotation they span the full bridged_area. Anchors are left at full bbox length (not clipped) to guarantee every scan line finds an anchor. Rectilinear and other straight-line patterns are unaffected. Known limitation: some bridge edges may still terminate over air in edge cases where the nearest synthetic anchor line is more than one infill spacing away from the bridge boundary. This will be addressed in a follow-up. * fix: anchor internal bridges to actual sparse infill Preserve real anchors across regions and align plane-path anchor origins with printed infill. Respect lower-layer rotation templates and model alignment, and sample curved bridge boundaries more finely. Add regression coverage for anchor alignment, bridge angles and region isolation, with Orca comments explaining the geometry constraints. Verified 175 FFF tests before the comment-only follow-up; preserve CRLF in modified files. * Fix internal bridge support contacts and separated infill origins Restore anchor contact after bridge smoothing and share per-body pattern origins between anchors and printed infill. Recompute origins when preparation settings change. Cover multiline counts 1, 2 and 3 and add regressions for printed bridge support, separated infill alignment and reslicing. * Add explicit standard headers to PrintObject tests * test: cover surface centering when infill settings change Verify top and bottom Archimedean Chords and Octagram Spiral paths after switching centering modes or toggling separated infills. Compare reslicing against fresh slicing and document dependent infill invalidation. * test: preserve directional surface infill when settings change * perf: index layer islands for connected-body detection * test: use public print pipeline for body centering checks
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
|
||||
#include "AABBTreeLines.hpp"
|
||||
#include "ExtrusionEntity.hpp"
|
||||
#include "FillBase.hpp"
|
||||
#include "Fill.hpp"
|
||||
#include "FillRectilinear.hpp"
|
||||
#include "FillLightning.hpp"
|
||||
#include "FillConcentricInternal.hpp"
|
||||
@@ -1234,6 +1234,33 @@ std::vector<SurfaceFill> group_fills(const Layer &layer, LockRegionParam &lock_p
|
||||
return surface_fills;
|
||||
}
|
||||
|
||||
// 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)
|
||||
{
|
||||
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 &&
|
||||
(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) {
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
return bbox;
|
||||
}
|
||||
|
||||
#ifdef SLIC3R_DEBUG_SLICE_PROCESSING
|
||||
void export_group_fills_to_svg(const char *path, const std::vector<SurfaceFill> &fills)
|
||||
{
|
||||
@@ -1353,19 +1380,9 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
|
||||
// Orca: Checking the filling of a centered surface by drawing for each model parts
|
||||
bool is_top_or_bottom = params.extrusion_role == erTopSolidInfill || params.extrusion_role == erBottomSurface;
|
||||
bool is_centered_infill = surface_fill.params.pattern == ipArchimedeanChords || surface_fill.params.pattern == ipOctagramSpiral;
|
||||
if (is_top_or_bottom) {
|
||||
params.center_of_surface_pattern = surface_fill.params.center_of_surface_pattern; // Orca: center of surface pattern
|
||||
}
|
||||
// Orca: Each_Model centers the pattern on each model part's bbox; Each_Surface / Each_Assembly
|
||||
// fall through to the default (whole-object) bounding box below.
|
||||
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_separable_infill_pattern(surface_fill.params.pattern) ||
|
||||
params.config->solid_infill_rotate_template != "" ||
|
||||
params.config->sparse_infill_rotate_template != "" );
|
||||
|
||||
if( surface_fill.params.pattern == ipLockedZag ) {
|
||||
params.locked_zag = true;
|
||||
params.infill_lock_depth = surface_fill.params.infill_lock_depth;
|
||||
@@ -1389,34 +1406,8 @@ void Layer::make_fills(FillAdaptive::Octree* adaptive_fill_octree, FillAdaptive:
|
||||
params.can_reverse = false;
|
||||
for (ExPolygon& expoly : surface_fill.expolygons) {
|
||||
|
||||
// Orca: separate infill / per-model pattern centering.
|
||||
//
|
||||
// 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.;
|
||||
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_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
|
||||
// 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));
|
||||
|
||||
f->no_overlap_expolygons = intersection_ex(surface_fill.no_overlap_expolygons, ExPolygons() = {expoly}, ApplySafetyOffset::Yes);
|
||||
if (params.symmetric_infill_y_axis) {
|
||||
@@ -1583,8 +1574,14 @@ Polylines Layer::generate_sparse_infill_polylines_for_anchoring(FillAdaptive::Oc
|
||||
params.multiline = surface_fill.params.multiline;
|
||||
params.gyroid_optimized = surface_fill.params.gyroid_optimized;
|
||||
params.smooth_factor = surface_fill.params.smooth_factor;
|
||||
// Orca: Match make_fills() when choosing the origin of plane-path patterns.
|
||||
// Without the sparse extrusion role, the filler uses each surface's bounds
|
||||
// instead of the object's bounds, so bridge anchors shift away from printed infill.
|
||||
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));
|
||||
// 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);
|
||||
|
||||
@@ -14,6 +14,12 @@ namespace Slic3r {
|
||||
|
||||
class ExtrusionEntityCollection;
|
||||
class LayerRegion;
|
||||
class PrintObject;
|
||||
|
||||
// Orca: Share the layer rotation calculation between infill generation and internal
|
||||
// bridge angle selection so both interpret rotation templates in the same way.
|
||||
double calculate_infill_rotation_angle(const PrintObject *object, size_t layer_id,
|
||||
const double &fixed_infill_angle, const std::string &template_string);
|
||||
|
||||
// An interface class to Perl, aggregating an instance of a Fill and a FillData.
|
||||
class Filler
|
||||
|
||||
@@ -21,9 +21,11 @@
|
||||
#include "TriangleMeshSlicer.hpp"
|
||||
#include "Utils.hpp"
|
||||
#include "Fill/FillAdaptive.hpp"
|
||||
#include "Fill/Fill.hpp"
|
||||
#include "Fill/FillLightning.hpp"
|
||||
#include "Format/STL.hpp"
|
||||
#include "format.hpp"
|
||||
#include "AABBTreeIndirect.hpp"
|
||||
#include "AABBTreeLines.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -672,6 +674,98 @@ void PrintObject::prepare_infill()
|
||||
} // for each region
|
||||
#endif /* SLIC3R_DEBUG_SLICE_PROCESSING */
|
||||
|
||||
// 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. Compute this before bridges so anchors and extrusion share the same origin.
|
||||
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;
|
||||
}
|
||||
}
|
||||
// Orca: 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); // Orca: 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];
|
||||
// Orca: 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; };
|
||||
// Orca: Index the smaller of two consecutive layers instead of scanning every
|
||||
// pair of islands. The tree prunes distant boxes on fragmented models; exact
|
||||
// polygon intersections still decide connectivity for the remaining candidates.
|
||||
for (size_t i = 0; i + 1 < nl; ++ i) {
|
||||
m_print->throw_if_canceled();
|
||||
size_t layer_a = i, layer_b = i + 1;
|
||||
if (m_layers[layer_a]->lslices.size() < m_layers[layer_b]->lslices.size())
|
||||
std::swap(layer_a, layer_b);
|
||||
const Layer *la = m_layers[layer_a], *lb = m_layers[layer_b];
|
||||
if (lb->lslices.empty())
|
||||
continue;
|
||||
|
||||
using IslandTree = AABBTreeIndirect::Tree<2, coord_t>;
|
||||
std::vector<AABBTreeIndirect::BoundingBoxWrapper> bboxes;
|
||||
bboxes.reserve(lb->lslices.size());
|
||||
for (size_t b = 0; b < lb->lslices.size(); ++ b)
|
||||
bboxes.emplace_back(b, lb->lslices_bboxes[b]);
|
||||
IslandTree tree;
|
||||
tree.build_modify_input(bboxes);
|
||||
for (size_t a = 0; a < la->lslices.size(); ++ a) {
|
||||
const IslandTree::BoundingBox query(la->lslices_bboxes[a].min, la->lslices_bboxes[a].max);
|
||||
AABBTreeIndirect::traverse(tree,
|
||||
[&query](const IslandTree::Node &node) { return node.bbox.intersects(query); },
|
||||
[&](const IslandTree::Node &node) {
|
||||
const size_t b = node.idx;
|
||||
// Orca: Tree boxes include an epsilon, so retain the original box
|
||||
// filter. Already-connected islands cannot change the partition
|
||||
// and need no further polygon intersection.
|
||||
if (la->lslices_bboxes[a].overlap(lb->lslices_bboxes[b]) &&
|
||||
find(offset[layer_a] + a) != find(offset[layer_b] + b) &&
|
||||
! intersection_ex(la->lslices[a], lb->lslices[b]).empty())
|
||||
unite(offset[layer_a] + a, offset[layer_b] + b);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
// Orca: 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]);
|
||||
// Orca: 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)];
|
||||
}
|
||||
}
|
||||
|
||||
// the following step needs to be done before combination because it may need
|
||||
// to remove only half of the combined infill
|
||||
this->bridge_over_infill();
|
||||
@@ -706,71 +800,6 @@ 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;
|
||||
|
||||
@@ -1401,8 +1430,6 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_anchor_max"
|
||||
|| opt_key == "top_surface_line_width"
|
||||
|| opt_key == "bottom_surface_density"
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
|| opt_key == "separated_infills"
|
||||
|| opt_key == "initial_layer_line_width"
|
||||
|| opt_key == "small_area_infill_flow_compensation"
|
||||
|| opt_key == "lateral_lattice_angle_1"
|
||||
@@ -1410,6 +1437,10 @@ bool PrintObject::invalidate_state_by_config_options(
|
||||
|| opt_key == "infill_overhang_angle") {
|
||||
steps.emplace_back(posInfill);
|
||||
} else if (opt_key == "sparse_infill_pattern"
|
||||
// Orca: Body centering now also determines bridge anchors during preparation.
|
||||
// Invalidating preparation also invalidates infill, including top/bottom surfaces.
|
||||
|| opt_key == "center_of_surface_pattern"
|
||||
|| opt_key == "separated_infills"
|
||||
|| opt_key == "sparse_infill_smooth_factor"
|
||||
|| opt_key == "symmetric_infill_y_axis"
|
||||
|| opt_key == "infill_shift_step"
|
||||
@@ -3009,21 +3040,12 @@ void PrintObject::bridge_over_infill()
|
||||
return diff(layers_sparse_infill, not_sparse_infill);
|
||||
};
|
||||
|
||||
// LAMBDA do determine optimal bridging angle
|
||||
auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors, InfillPattern dominant_pattern, double infill_direction) {
|
||||
// Orca: Derive the fallback bridge direction from the supplied anchor geometry.
|
||||
// Pattern-specific angle selection belongs at the call site, where the supporting
|
||||
// layer and region are known; this helper must not override it with a base config angle.
|
||||
auto determine_bridging_angle = [](const Polygons &bridged_area, const Lines &anchors) {
|
||||
AABBTreeLines::LinesDistancer<Line> lines_tree(anchors);
|
||||
|
||||
// Orca: since 3D Honeycomb was "fixed" by forcing coordf_t layerHeight = scale_(1.0), this is no longer needed.
|
||||
// CorssHatch also does not need fixed angle.
|
||||
//
|
||||
// Check it the infill that require a fixed infill angle.
|
||||
//switch (dominant_pattern) {
|
||||
//case ip3DHoneycomb:
|
||||
//case ipCrossHatch:
|
||||
// return (infill_direction + 45.0) * 2.0 * M_PI / 360.;
|
||||
//default: break;
|
||||
//}
|
||||
|
||||
std::map<double, int> counted_directions;
|
||||
for (const Polygon &p : bridged_area) {
|
||||
double acc_distance = 0;
|
||||
@@ -3089,18 +3111,15 @@ void PrintObject::bridge_over_infill()
|
||||
if (bridging_angle == 0) {
|
||||
bridging_angle = 0.001;
|
||||
}
|
||||
switch (dominant_pattern) {
|
||||
case ipHilbertCurve: bridging_angle += 0.25 * PI; break;
|
||||
case ipOctagramSpiral: bridging_angle += (1.0 / 16.0) * PI; break;
|
||||
default: break;
|
||||
}
|
||||
|
||||
return bridging_angle;
|
||||
};
|
||||
|
||||
// LAMBDA that will fill given polygons with lines, exapand the lines to the nearest anchor, and reconstruct polygons from the newly
|
||||
// generated lines
|
||||
auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle) {
|
||||
// Orca: Extend scan sections to the nearest anchors and reconstruct the bridge area.
|
||||
// scan_spacing controls boundary sampling independently of the extrusion spacing;
|
||||
// anchoring overlap and smoothing thresholds still use the physical bridging flow.
|
||||
auto construct_anchored_polygon = [](Polygons bridged_area, Lines anchors, const Flow &bridging_flow, double bridging_angle,
|
||||
coord_t scan_spacing, bool restore_anchors = false) {
|
||||
auto lines_rotate = [](Lines &lines, double cos_angle, double sin_angle) {
|
||||
for (Line &l : lines) {
|
||||
double ax = double(l.a.x());
|
||||
@@ -3127,12 +3146,12 @@ void PrintObject::bridge_over_infill()
|
||||
BoundingBox bb_x = get_extents(bridged_area);
|
||||
BoundingBox bb_y = get_extents(anchors);
|
||||
|
||||
const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + bridging_flow.scaled_spacing() - 1) / bridging_flow.scaled_spacing();
|
||||
const size_t n_vlines = (bb_x.max.x() - bb_x.min.x() + scan_spacing - 1) / scan_spacing;
|
||||
std::vector<Line> vertical_lines(n_vlines);
|
||||
for (size_t i = 0; i < n_vlines; i++) {
|
||||
// Orca: Make sure the line is placed in the middle of the extrusion
|
||||
// coord_t x = bb_x.min.x() + i * bridging_flow.scaled_spacing();
|
||||
coord_t x = bb_x.min.x() + (i + 0.5) * bridging_flow.scaled_spacing();
|
||||
// Orca: Sample the center of each reconstructed strip. Its edges lie
|
||||
// half a scan step away, even when the sampling is finer than extrusion.
|
||||
coord_t x = bb_x.min.x() + (i + 0.5) * scan_spacing;
|
||||
coord_t y_min = bb_y.min.y() - bridging_flow.scaled_spacing();
|
||||
coord_t y_max = bb_y.max.y() + bridging_flow.scaled_spacing();
|
||||
vertical_lines[i].a = Point{x, y_min};
|
||||
@@ -3155,7 +3174,11 @@ void PrintObject::bridge_over_infill()
|
||||
auto anchors_intersections = anchors_and_walls_tree.intersections_with_line<true>(vertical_lines[i]);
|
||||
|
||||
for (Line §ion : polygon_sections[i]) {
|
||||
auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a,
|
||||
// Orca: A repaired boundary may already overlap its anchor by one flow width.
|
||||
// Include that overlap in the search so restoring rounded corners does not
|
||||
// extend every already anchored section into the next sparse infill cell.
|
||||
const coord_t overlap = restore_anchors ? bridging_flow.scaled_width() + SCALED_EPSILON : 0;
|
||||
auto maybe_below_anchor = std::upper_bound(anchors_intersections.rbegin(), anchors_intersections.rend(), section.a + Point{0, overlap},
|
||||
[](const Point &a, const std::pair<Point, size_t> &b) {
|
||||
return a.y() > b.first.y();
|
||||
});
|
||||
@@ -3164,7 +3187,7 @@ void PrintObject::bridge_over_infill()
|
||||
section.a.y() -= bridging_flow.scaled_width() * (0.5 + 0.5);
|
||||
}
|
||||
|
||||
auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b,
|
||||
auto maybe_upper_anchor = std::upper_bound(anchors_intersections.begin(), anchors_intersections.end(), section.b - Point{0, overlap},
|
||||
[](const Point &a, const std::pair<Point, size_t> &b) {
|
||||
return a.y() < b.first.y();
|
||||
});
|
||||
@@ -3194,7 +3217,9 @@ void PrintObject::bridge_over_infill()
|
||||
});
|
||||
}
|
||||
|
||||
// reconstruct polygon from polygon sections
|
||||
// Orca: Reconstruct the polygon from scan sections. At discontinuities and
|
||||
// strip starts/ends, use half the scan step for the X offsets; using half an
|
||||
// extrusion spacing would overlap the finer strips and distort curved anchors.
|
||||
struct TracedPoly
|
||||
{
|
||||
Points lows;
|
||||
@@ -3220,8 +3245,8 @@ void PrintObject::bridge_over_infill()
|
||||
36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) {
|
||||
traced_poly.lows.push_back(candidate->a);
|
||||
} else {
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a - Point{scan_spacing / 2, 0});
|
||||
traced_poly.lows.push_back(candidate->a);
|
||||
}
|
||||
|
||||
@@ -3229,8 +3254,8 @@ void PrintObject::bridge_over_infill()
|
||||
36.0 * double(bridging_flow.scaled_spacing()) * bridging_flow.scaled_spacing()) {
|
||||
traced_poly.highs.push_back(candidate->b);
|
||||
} else {
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b - Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(candidate->b);
|
||||
}
|
||||
segment_added = true;
|
||||
@@ -3238,9 +3263,9 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
|
||||
if (!segment_added) {
|
||||
// Zero overlapping segments, we just close this polygon
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
// Orca: No section continues this strip; close at its right edge.
|
||||
traced_poly.lows.push_back(traced_poly.lows.back() + Point{scan_spacing / 2, 0});
|
||||
traced_poly.highs.push_back(traced_poly.highs.back() + Point{scan_spacing / 2, 0});
|
||||
Polygon &new_poly = expanded_bridged_area.emplace_back(std::move(traced_poly.lows));
|
||||
new_poly.points.insert(new_poly.points.end(), traced_poly.highs.rbegin(), traced_poly.highs.rend());
|
||||
traced_poly.lows.clear();
|
||||
@@ -3255,9 +3280,9 @@ void PrintObject::bridge_over_infill()
|
||||
for (const auto &segment : polygon_slice) {
|
||||
if (used_segments.find(&segment) == used_segments.end()) {
|
||||
TracedPoly &new_tp = current_traced_polys.emplace_back();
|
||||
new_tp.lows.push_back(segment.a - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
new_tp.lows.push_back(segment.a - Point{scan_spacing / 2, 0});
|
||||
new_tp.lows.push_back(segment.a);
|
||||
new_tp.highs.push_back(segment.b - Point{bridging_flow.scaled_spacing() / 2, 0});
|
||||
new_tp.highs.push_back(segment.b - Point{scan_spacing / 2, 0});
|
||||
new_tp.highs.push_back(segment.b);
|
||||
}
|
||||
}
|
||||
@@ -3364,7 +3389,10 @@ void PrintObject::bridge_over_infill()
|
||||
total_fill_area = closing(total_fill_area, float(SCALED_EPSILON));
|
||||
expansion_area = closing(expansion_area, float(SCALED_EPSILON));
|
||||
expansion_area = intersection(expansion_area, deep_infill_area);
|
||||
Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing));
|
||||
// Orca: Preserve the real lower-layer anchors for every candidate in this
|
||||
// layer. Replacing this shared set for one pattern also changes later regions,
|
||||
// and synthetic straight lines can claim support where no infill is printed.
|
||||
const Polylines anchors = intersection_pl(infill_lines[lidx - 1], shrink(expansion_area, spacing));
|
||||
Polygons internal_unsupported_area = shrink(deep_infill_area, spacing * 4.5);
|
||||
|
||||
#ifdef DEBUG_BRIDGE_OVER_INFILL
|
||||
@@ -3375,6 +3403,9 @@ void PrintObject::bridge_over_infill()
|
||||
std::vector<CandidateSurface> expanded_surfaces;
|
||||
expanded_surfaces.reserve(surfaces_by_layer[lidx].size());
|
||||
for (const CandidateSurface &candidate : surfaces_by_layer[lidx]) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const bool turning_pattern = region_config.sparse_infill_pattern == ipHilbertCurve ||
|
||||
region_config.sparse_infill_pattern == ipOctagramSpiral;
|
||||
const Flow &flow = candidate.region->bridging_flow(frSolidInfill, true);
|
||||
Polygons area_to_be_bridge = expand(candidate.new_polys, flow.scaled_spacing());
|
||||
area_to_be_bridge = intersection(area_to_be_bridge, deep_infill_area);
|
||||
@@ -3403,20 +3434,40 @@ void PrintObject::bridge_over_infill()
|
||||
to_lines(area_to_be_bridge), to_lines(boundary_plines), to_lines(anchors), to_lines(expansion_area));
|
||||
#endif
|
||||
|
||||
double bridging_angle = 0;
|
||||
if (!anchors.empty()) {
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors),
|
||||
candidate.region->region().config().sparse_infill_pattern.value,
|
||||
candidate.region->region().config().infill_direction.value);
|
||||
} else {
|
||||
// use expansion boundaries as anchors.
|
||||
// Also, use Infill pattern that is neutral for angle determination, since there are no infill lines.
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(boundary_plines), InfillPattern::ipLine, 0);
|
||||
double bridging_angle = -1.;
|
||||
if (!anchors.empty() && turning_pattern) {
|
||||
// Orca: Keep adjacent bridges over Hilbert/Octagram aligned despite
|
||||
// their many local turning directions. Use the lower layer's rotation,
|
||||
// since that is the infill supporting the bridge, not the current layer's.
|
||||
for (const LayerRegion *lower_region : layer->lower_layer->regions()) {
|
||||
// Orca: Apply the configured direction only if the same region has
|
||||
// sparse infill below this bridge. A height modifier may put another
|
||||
// pattern underneath, requiring the geometry-based fallback below.
|
||||
if (&lower_region->region() != &candidate.region->region() ||
|
||||
intersection(area_to_be_bridge, to_polygons(lower_region->fill_surfaces.filter_by_type(stInternal))).empty())
|
||||
continue;
|
||||
bridging_angle = calculate_infill_rotation_angle(po, layer->lower_layer->id(), region_config.infill_direction.value,
|
||||
region_config.sparse_infill_rotate_template.value) + 0.5 * PI;
|
||||
// Orca: Apply model alignment as infill generation does, then normalize
|
||||
// the undirected bridge angle to [0, PI), including negative rotations.
|
||||
if (region_config.align_infill_direction_to_model) {
|
||||
const auto &m = po->trafo().matrix();
|
||||
bridging_angle += std::atan2(double(m(1, 0)), double(m(0, 0)));
|
||||
}
|
||||
bridging_angle = std::fmod(bridging_angle, PI);
|
||||
if (bridging_angle < 0.)
|
||||
bridging_angle += PI;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Orca: A different region below (e.g. a height modifier) needs the actual anchor
|
||||
// directions. When there are no sparse anchors, use the expansion boundaries.
|
||||
if (bridging_angle < 0.)
|
||||
bridging_angle = determine_bridging_angle(area_to_be_bridge, to_lines(anchors.empty() ? boundary_plines : anchors));
|
||||
|
||||
// ORCA: Internal bridge angle override
|
||||
// Orca: Preserve the user's absolute or relative internal bridge angle
|
||||
// override after automatic direction selection.
|
||||
if (candidate.region->region().config().internal_bridge_angle.value > 0) {
|
||||
const auto ®ion_config = candidate.region->region().config();
|
||||
const double custom_angle_rad = Geometry::deg2rad(region_config.internal_bridge_angle.value);
|
||||
if (region_config.relative_bridge_angle.value)
|
||||
bridging_angle += custom_angle_rad;
|
||||
@@ -3429,11 +3480,19 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
}
|
||||
|
||||
// Orca: Changing the bridge direction must not change its physical supports.
|
||||
// Extend to actual sparse infill or the existing boundary anchors, never to
|
||||
// a synthetic grid that merely has the same nominal angle and spacing.
|
||||
boundary_plines.insert(boundary_plines.end(), anchors.begin(), anchors.end());
|
||||
if (!lightning_area.empty() && !intersection(area_to_be_bridge, lightning_area).empty()) {
|
||||
boundary_plines = intersection_pl(boundary_plines, expand(area_to_be_bridge, scale_(10)));
|
||||
}
|
||||
Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle);
|
||||
// Orca: Use four samples per extrusion spacing for Hilbert/Octagram so the
|
||||
// reconstructed boundary follows rounded anchors instead of cutting corners.
|
||||
// Keep the original step for other patterns and at least one coordinate unit
|
||||
// after integer division. This changes boundary accuracy, not infill density.
|
||||
const coord_t scan_spacing = std::max(coord_t(1), flow.scaled_spacing() / (turning_pattern ? 4 : 1));
|
||||
Polygons bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing);
|
||||
|
||||
// Check collision with other expanded surfaces
|
||||
{
|
||||
@@ -3447,7 +3506,9 @@ void PrintObject::bridge_over_infill()
|
||||
}
|
||||
}
|
||||
if (reconstruct) {
|
||||
bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle);
|
||||
// Orca: Retain the same sampling accuracy when matching a nearby
|
||||
// bridge's direction; rebuilding must not lose the curved supports.
|
||||
bridging_area = construct_anchored_polygon(area_to_be_bridge, to_lines(boundary_plines), flow, bridging_angle, scan_spacing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3455,6 +3516,13 @@ void PrintObject::bridge_over_infill()
|
||||
// bridging_area = opening(bridging_area, flow.scaled_spacing());
|
||||
bridging_area = opening(bridging_area, flow.scaled_spacing() * 0.75);
|
||||
bridging_area = closing(bridging_area, flow.scaled_spacing());
|
||||
// Orca: Opening/closing can pull rounded bridge ends away from their real
|
||||
// supports. Restore those contacts after smoothing, preserving the cleaned
|
||||
// area and the selected angle; do not smooth the restored contacts again.
|
||||
if (turning_pattern && !bridging_area.empty()) {
|
||||
bridging_area = union_(bridging_area, construct_anchored_polygon(bridging_area, to_lines(boundary_plines), flow,
|
||||
bridging_angle, scan_spacing, true));
|
||||
}
|
||||
bridging_area = intersection(bridging_area, limiting_area);
|
||||
bridging_area = intersection(bridging_area, total_fill_area);
|
||||
bridging_area = diff(bridging_area, total_top_area);
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/AABBTreeLines.hpp"
|
||||
#include "libslic3r/Fill/Fill.hpp"
|
||||
#include "libslic3r/Flow.hpp"
|
||||
#include "libslic3r/Geometry.hpp"
|
||||
@@ -1229,3 +1230,68 @@ TEST_CASE("Smoothing multiline lightning infill keeps its outlines connected", "
|
||||
REQUIRE(smooth.point_count > sharp.point_count);
|
||||
REQUIRE(smooth.sharp_turns < sharp.sharp_turns);
|
||||
}
|
||||
|
||||
TEST_CASE("Sparse plane-path anchors match the printed infill", "[Fill][InternalBridge][Regression]")
|
||||
{
|
||||
// Orca: Compare generated anchors with actual extrusion across plane-path patterns,
|
||||
// smoothing, multiline and rotations; an origin shift must not pass as valid support.
|
||||
const std::string pattern = GENERATE("hilbertcurve", "octagramspiral", "archimedeanchords");
|
||||
const std::string smoothing = GENERATE("0%", "100%");
|
||||
const int multiline = GENERATE(1, 2);
|
||||
const bool rotated = GENERATE(false, true);
|
||||
const bool separated = GENERATE(false, true);
|
||||
CAPTURE(pattern, smoothing, multiline, rotated, separated);
|
||||
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
|
||||
{"sparse_infill_density", "15%"},
|
||||
{"sparse_infill_smooth_factor", smoothing},
|
||||
{"fill_multiline", multiline},
|
||||
{"infill_direction", 45},
|
||||
{"sparse_infill_rotate_template", rotated ? "0,25,50" : ""},
|
||||
{"align_infill_direction_to_model", rotated},
|
||||
{"separated_infills", separated},
|
||||
{"top_shell_layers", 0},
|
||||
{"bottom_shell_layers", 0},
|
||||
{"top_shell_thickness", 0},
|
||||
{"bottom_shell_thickness", 0},
|
||||
{"layer_height", 0.2},
|
||||
{"initial_layer_print_height", 0.2},
|
||||
{"resolution", 0.012}});
|
||||
Print print;
|
||||
Model model;
|
||||
TriangleMesh mesh = make_cube(30, 24, 1);
|
||||
if (separated) {
|
||||
// Orca: Two disconnected bodies in one object must each use their own infill origin.
|
||||
TriangleMesh second = make_cube(30, 24, 1);
|
||||
second.translate(50, 0, 0);
|
||||
mesh.merge(second);
|
||||
}
|
||||
Slic3r::Test::init_print({mesh}, print, model, config, nullptr, false);
|
||||
if (rotated) {
|
||||
model.objects.front()->instances.front()->set_rotation(Vec3d(0., 0., Geometry::deg2rad(23.)));
|
||||
print.apply(model, config);
|
||||
}
|
||||
print.process();
|
||||
|
||||
const Layer &layer = *print.objects().front()->get_layer(4);
|
||||
Polylines printed;
|
||||
for (const LayerRegion *region : layer.regions())
|
||||
for (const ExtrusionEntity *entity : region->fills.flatten().entities)
|
||||
if (entity->role() == erInternalInfill)
|
||||
entity->collect_polylines(printed);
|
||||
REQUIRE_FALSE(printed.empty());
|
||||
const AABBTreeLines::LinesDistancer<Line> printed_tree(to_lines(printed));
|
||||
|
||||
// Orca: Exclude perimeter connections: anchoring and extrusion can trim those differently.
|
||||
const Polylines anchors = intersection_pl(layer.generate_sparse_infill_polylines_for_anchoring(nullptr, nullptr, nullptr),
|
||||
shrink(to_polygons(layer.lslices), scale_(3.)));
|
||||
REQUIRE_FALSE(anchors.empty());
|
||||
double max_distance = 0.;
|
||||
for (const Polyline &path : anchors)
|
||||
for (const Point &point : path.equally_spaced_points(scale_(0.25)))
|
||||
max_distance = std::max(max_distance, printed_tree.distance_from_lines<false>(point));
|
||||
// Orca: Allow only the configured simplification tolerance; infill-scale offsets
|
||||
// would hide anchors that no longer coincide with printed lines.
|
||||
CHECK(unscale<double>(max_distance) <= config.opt_float("resolution"));
|
||||
}
|
||||
|
||||
@@ -4,11 +4,18 @@
|
||||
#include "libslic3r/Print.hpp"
|
||||
#include "libslic3r/Layer.hpp"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
#include "libslic3r/ClipperUtils.hpp"
|
||||
#include "libslic3r/AABBTreeLines.hpp"
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
|
||||
#include <cmath>
|
||||
#include <iterator>
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
using namespace Slic3r::Test;
|
||||
@@ -130,3 +137,438 @@ TEST_CASE("Initial layer height is honored", "[PrintObject]")
|
||||
REQUIRE_THAT(*layer_zs.begin(), Catch::Matchers::WithinAbs(0.3, 1e-4));
|
||||
REQUIRE_THAT(*std::next(layer_zs.begin()), Catch::Matchers::WithinAbs(0.5, 1e-4));
|
||||
}
|
||||
|
||||
static TriangleMesh internal_bridge_step()
|
||||
{
|
||||
// Orca: The smaller tower leaves a shoulder whose solid skin needs internal bridges
|
||||
// over the sparse infill in the base, without relying on an external model file.
|
||||
TriangleMesh mesh = make_cube(30, 24, 3);
|
||||
TriangleMesh tower = make_cube(14, 10, 1);
|
||||
tower.translate(8, 7, 3);
|
||||
mesh.merge(tower);
|
||||
return mesh;
|
||||
}
|
||||
|
||||
static DynamicPrintConfig internal_bridge_config(const std::string &pattern, int multiline)
|
||||
{
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({{"sparse_infill_pattern", pattern},
|
||||
{"fill_multiline", multiline},
|
||||
{"sparse_infill_density", "15%"},
|
||||
{"sparse_infill_smooth_factor", "100%"},
|
||||
{"infill_direction", 45},
|
||||
{"internal_bridge_angle", 0},
|
||||
{"thick_internal_bridges", true},
|
||||
{"top_shell_layers", 3},
|
||||
{"bottom_shell_layers", 2},
|
||||
{"top_shell_thickness", 0},
|
||||
{"bottom_shell_thickness", 0},
|
||||
{"layer_height", 0.2},
|
||||
{"initial_layer_print_height", 0.2}});
|
||||
return config;
|
||||
}
|
||||
|
||||
TEST_CASE("Internal bridge angles follow the lower infill layer and model rotation", "[PrintObject][InternalBridge][Regression]")
|
||||
{
|
||||
const std::string pattern = GENERATE("hilbertcurve", "octagramspiral");
|
||||
// Orca: Cover both a central line (odd counts) and offset pairs (even counts).
|
||||
const int multiline = GENERATE(1, 2, 3);
|
||||
CAPTURE(multiline);
|
||||
const double rotation = GENERATE(23., -123.);
|
||||
const std::vector<double> cycle{10., 30., 70.};
|
||||
auto config = internal_bridge_config(pattern, multiline);
|
||||
config.set_deserialize_strict({{"sparse_infill_rotate_template", "10,30,70"},
|
||||
{"align_infill_direction_to_model", true},
|
||||
{"separated_infills", false}});
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({internal_bridge_step()}, print, model, config, nullptr, false);
|
||||
model.objects.front()->instances.front()->set_rotation(Vec3d(0., 0., Geometry::deg2rad(rotation)));
|
||||
print.apply(model, config);
|
||||
print.process();
|
||||
const PrintObject &object = *print.objects().front();
|
||||
size_t bridges = 0;
|
||||
for (size_t i = 1; i < object.layer_count(); ++i) {
|
||||
// Orca: The support is one layer below the bridge. Check the template and model
|
||||
// rotation together, including normalization when the resulting angle is negative.
|
||||
double expected = std::fmod(cycle[(i - 1) % cycle.size()] + 90. + rotation, 180.);
|
||||
if (expected < 0.) expected += 180.;
|
||||
for (const LayerRegion *region : object.get_layer(i)->regions())
|
||||
for (const Surface *surface : region->fill_surfaces.filter_by_type(stInternalBridge)) {
|
||||
CAPTURE(pattern, rotation, i);
|
||||
CHECK_THAT(Geometry::rad2deg(surface->bridge_angle), Catch::Matchers::WithinAbs(expected, 0.001));
|
||||
++bridges;
|
||||
}
|
||||
}
|
||||
REQUIRE(bridges > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Turning infill does not replace the anchors of another region", "[PrintObject][InternalBridge][Regression]")
|
||||
{
|
||||
// Orca: Keep the right-hand region fixed while changing the left-hand pattern in the
|
||||
// same object. Its bridge areas must be independent of a previous candidate's anchors.
|
||||
const int multiline = GENERATE(1, 2, 3);
|
||||
CAPTURE(multiline);
|
||||
auto right_bridges = [multiline](const std::string &left_pattern) {
|
||||
auto config = internal_bridge_config(left_pattern, multiline);
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({internal_bridge_step()}, print, model, config, nullptr, false);
|
||||
TriangleMesh right = internal_bridge_step();
|
||||
right.translate(50, 0, 0);
|
||||
ModelVolume *volume = model.objects.front()->add_volume(std::move(right));
|
||||
volume->config.set_key_value("sparse_infill_pattern", new ConfigOptionEnum<InfillPattern>(ipRectilinear));
|
||||
volume->config.set_key_value("infill_direction", new ConfigOptionFloat(17.));
|
||||
print.apply(model, config);
|
||||
print.process();
|
||||
std::map<size_t, Polygons> result;
|
||||
const PrintObject &object = *print.objects().front();
|
||||
for (size_t i = 0; i < object.layer_count(); ++i)
|
||||
for (const LayerRegion *region : object.get_layer(i)->regions())
|
||||
if (region->region().config().infill_direction == 17.)
|
||||
polygons_append(result[i], to_polygons(region->fill_surfaces.filter_by_type(stInternalBridge)));
|
||||
return result;
|
||||
};
|
||||
const auto baseline = right_bridges("rectilinear");
|
||||
const auto actual = right_bridges(GENERATE("hilbertcurve", "octagramspiral"));
|
||||
REQUIRE(actual.size() == baseline.size());
|
||||
double total_area = 0.;
|
||||
for (const auto &[layer, expected] : baseline) {
|
||||
CAPTURE(layer);
|
||||
const auto &polys = actual.at(layer);
|
||||
CHECK(area(diff(expected, polys)) < scaled<double>(1.) * scaled<double>(1.) * 1e-6);
|
||||
CHECK(area(diff(polys, expected)) < scaled<double>(1.) * scaled<double>(1.) * 1e-6);
|
||||
total_area += area(expected);
|
||||
}
|
||||
REQUIRE(total_area > 0.);
|
||||
}
|
||||
|
||||
TEST_CASE("Rounded internal bridges end on printed support", "[PrintObject][InternalBridge][Regression]")
|
||||
{
|
||||
const std::string pattern = GENERATE("hilbertcurve", "octagramspiral");
|
||||
const bool separated = GENERATE(false, true);
|
||||
CAPTURE(pattern, separated);
|
||||
auto config = internal_bridge_config(pattern, 1);
|
||||
config.set_deserialize_strict({{"infill_wall_overlap", "0%"}, {"separated_infills", separated}});
|
||||
TriangleMesh mesh = internal_bridge_step();
|
||||
if (separated) {
|
||||
TriangleMesh second = internal_bridge_step();
|
||||
second.translate(50, 0, 0);
|
||||
mesh.merge(second);
|
||||
}
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({mesh}, print, model, config, nullptr, false);
|
||||
print.process();
|
||||
|
||||
// Orca: Check final extrusion endpoints after polygon cleanup and fill generation.
|
||||
// A correct bridge angle and correct sparse anchors alone do not guarantee contact.
|
||||
const PrintObject &object = *print.objects().front();
|
||||
size_t checked = 0;
|
||||
for (size_t i = 1; i < object.layer_count(); ++i) {
|
||||
Polygons support;
|
||||
Polylines walls;
|
||||
for (const LayerRegion *region : object.get_layer(i - 1)->regions()) {
|
||||
region->perimeters.polygons_covered_by_width(support, 0.f);
|
||||
region->fills.polygons_covered_by_width(support, 0.f);
|
||||
region->perimeters.collect_polylines(walls);
|
||||
}
|
||||
REQUIRE_FALSE(support.empty());
|
||||
const AABBTreeLines::LinesDistancer<Line> support_tree(to_lines(union_(support)));
|
||||
const AABBTreeLines::LinesDistancer<Line> wall_tree(to_lines(walls));
|
||||
for (const LayerRegion *region : object.get_layer(i)->regions())
|
||||
for (const ExtrusionEntity *entity : region->fills.flatten().entities) {
|
||||
if (entity->role() != erInternalBridgeInfill)
|
||||
continue;
|
||||
const auto *path = dynamic_cast<const ExtrusionPath *>(entity);
|
||||
REQUIRE(path != nullptr);
|
||||
for (const Line &line : path->polyline.to_polyline().lines()) {
|
||||
// Orca: Sample span ends, excluding short connectors and wall overlap.
|
||||
if (line.length() < scale_(std::max(0.7, 3. * path->width)))
|
||||
continue;
|
||||
for (const Point &point : {line.a, line.b}) {
|
||||
if (wall_tree.distance_from_lines<false>(point) <= scale_(0.5))
|
||||
continue;
|
||||
CAPTURE(i, point.x(), point.y());
|
||||
const double gap = unscale<double>(support_tree.distance_from_lines<true>(point)) - 0.5 * path->width;
|
||||
CHECK(gap <= 0.1);
|
||||
++checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
REQUIRE(checked > 0);
|
||||
}
|
||||
|
||||
TEST_CASE("Enabling separated infill recomputes body origins", "[PrintObject][InternalBridge][Regression]")
|
||||
{
|
||||
const std::string pattern = GENERATE("hilbertcurve", "octagramspiral", "archimedeanchords");
|
||||
CAPTURE(pattern);
|
||||
auto footprint = [&](bool reslice) {
|
||||
auto config = internal_bridge_config(pattern, 2);
|
||||
config.set_deserialize_strict({{"separated_infills", !reslice}});
|
||||
TriangleMesh mesh = internal_bridge_step();
|
||||
TriangleMesh second = internal_bridge_step();
|
||||
second.translate(50, 0, 0);
|
||||
mesh.merge(second);
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({mesh}, print, model, config, nullptr, false);
|
||||
print.process();
|
||||
if (reslice) {
|
||||
// Orca: Enabling centering after a completed slice must rebuild the body
|
||||
// origins now shared by bridge preparation and printed infill.
|
||||
config.set_deserialize_strict({{"separated_infills", true}});
|
||||
print.apply(model, config);
|
||||
print.process();
|
||||
}
|
||||
Polygons result;
|
||||
for (const LayerRegion *region : print.objects().front()->get_layer(4)->regions())
|
||||
region->fills.polygons_covered_by_width(result, 0.f);
|
||||
return union_(result);
|
||||
};
|
||||
const Polygons fresh = footprint(false);
|
||||
const Polygons resliced = footprint(true);
|
||||
REQUIRE_FALSE(fresh.empty());
|
||||
CHECK(area(diff(fresh, resliced)) < scaled<double>(1.) * scaled<double>(1.) * 1e-6);
|
||||
CHECK(area(diff(resliced, fresh)) < scaled<double>(1.) * scaled<double>(1.) * 1e-6);
|
||||
}
|
||||
|
||||
TEST_CASE("Surface centering survives changes to separated infill settings", "[PrintObject][SurfaceInfill][Regression]")
|
||||
{
|
||||
const std::string pattern = GENERATE("archimedeanchords", "octagramspiral");
|
||||
const std::string initial_center = GENERATE("each_surface", "each_model", "each_assembly");
|
||||
const std::string final_center = GENERATE("each_surface", "each_model", "each_assembly");
|
||||
const bool separated = GENERATE(false, true);
|
||||
const std::string top_order = GENERATE("default", "outward", "inward");
|
||||
const std::string bottom_order = top_order == "outward" ? "inward" : top_order == "inward" ? "outward" : "default";
|
||||
const std::string density = GENERATE("80%", "100%");
|
||||
const bool change_center = initial_center != final_center;
|
||||
CAPTURE(pattern, initial_center, final_center, separated, top_order, bottom_order, density);
|
||||
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({{"top_surface_pattern", pattern},
|
||||
{"bottom_surface_pattern", pattern},
|
||||
{"top_surface_fill_order", top_order},
|
||||
{"bottom_surface_fill_order", bottom_order},
|
||||
{"top_surface_density", density},
|
||||
{"bottom_surface_density", density},
|
||||
{"center_of_surface_pattern", initial_center},
|
||||
{"separated_infills", change_center ? separated : !separated},
|
||||
{"sparse_infill_pattern", "rectilinear"},
|
||||
{"sparse_infill_density", "15%"},
|
||||
{"top_shell_layers", 2},
|
||||
{"bottom_shell_layers", 2},
|
||||
{"top_shell_thickness", 0},
|
||||
{"bottom_shell_thickness", 0},
|
||||
{"layer_height", 0.2},
|
||||
{"initial_layer_print_height", 0.2}});
|
||||
|
||||
// Orca: Two disconnected bodies exercise per-body centering. The offset tower also
|
||||
// makes each-surface and each-model centering differ on the top surfaces.
|
||||
TriangleMesh mesh = make_cube(30, 24, 2);
|
||||
TriangleMesh tower = make_cube(12, 10, 1);
|
||||
tower.translate(4, 3, 2);
|
||||
mesh.merge(tower);
|
||||
TriangleMesh second = mesh;
|
||||
second.translate(50, 0, 0);
|
||||
mesh.merge(second);
|
||||
|
||||
// Orca: Equal footprints can hide reordered or reversed paths. Retain their point
|
||||
// sequences and ordering protection to cover the directional surface behavior too.
|
||||
struct SurfaceFillSnapshot {
|
||||
std::map<bool, std::vector<Points>> paths;
|
||||
bool protected_order = true;
|
||||
};
|
||||
auto surface_fills = [](const Print &print) {
|
||||
std::map<std::pair<size_t, ExtrusionRole>, SurfaceFillSnapshot> result;
|
||||
const PrintObject &object = *print.objects().front();
|
||||
for (size_t i = 0; i < object.layer_count(); ++i) {
|
||||
auto collect = [&](const auto &self, const ExtrusionEntity &entity, bool no_sort) -> void {
|
||||
if (const auto *collection = dynamic_cast<const ExtrusionEntityCollection *>(&entity)) {
|
||||
for (const ExtrusionEntity *child : collection->entities)
|
||||
self(self, *child, no_sort || collection->no_sort);
|
||||
} else if (entity.role() == erTopSolidInfill || entity.role() == erBottomSurface) {
|
||||
const auto *path = dynamic_cast<const ExtrusionPath *>(&entity);
|
||||
REQUIRE(path != nullptr);
|
||||
auto &snapshot = result[{i, entity.role()}];
|
||||
// Orca: The centered test model has one body on either side of X=0.
|
||||
// Their traversal order may vary; preserve path order within each body.
|
||||
Points points = path->polyline.to_polyline().points;
|
||||
REQUIRE_FALSE(points.empty());
|
||||
snapshot.paths[points.front().x() > 0].push_back(std::move(points));
|
||||
snapshot.protected_order &= no_sort && !path->can_reverse();
|
||||
}
|
||||
};
|
||||
for (const LayerRegion *region : object.get_layer(i)->regions())
|
||||
collect(collect, region->fills, false);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({mesh}, print, model, config, nullptr, false);
|
||||
print.process();
|
||||
const auto initial = surface_fills(print);
|
||||
config.set_deserialize_strict({{"center_of_surface_pattern", final_center}, {"separated_infills", separated}});
|
||||
print.apply(model, config);
|
||||
// Orca: Preparation owns the body origins, and its invalidation must also force
|
||||
// regeneration of top/bottom extrusion paths, even when sparse infill is unchanged.
|
||||
CHECK_FALSE(print.objects().front()->is_step_done(posPrepareInfill));
|
||||
CHECK_FALSE(print.objects().front()->is_step_done(posInfill));
|
||||
print.process();
|
||||
const auto resliced = surface_fills(print);
|
||||
|
||||
Print fresh_print;
|
||||
Model fresh_model;
|
||||
init_print({mesh}, fresh_print, fresh_model, config, nullptr, false);
|
||||
fresh_print.process();
|
||||
const auto fresh = surface_fills(fresh_print);
|
||||
REQUIRE_FALSE(fresh.empty());
|
||||
REQUIRE(resliced.size() == fresh.size());
|
||||
std::set<ExtrusionRole> roles;
|
||||
bool changed_paths = false;
|
||||
for (const auto &entry : fresh) {
|
||||
CAPTURE(entry.first.first, entry.first.second);
|
||||
REQUIRE_FALSE(entry.second.paths.empty());
|
||||
roles.insert(entry.first.second);
|
||||
REQUIRE(resliced.count(entry.first) == 1);
|
||||
REQUIRE(initial.count(entry.first) == 1);
|
||||
const auto &actual = resliced.at(entry.first);
|
||||
const auto &expected = entry.second;
|
||||
const auto &before = initial.at(entry.first);
|
||||
CHECK((actual.paths == expected.paths));
|
||||
if (!change_center)
|
||||
CHECK((actual.paths == before.paths));
|
||||
if (top_order != "default") {
|
||||
CHECK(expected.protected_order);
|
||||
CHECK(actual.protected_order);
|
||||
CHECK(before.protected_order);
|
||||
}
|
||||
changed_paths |= expected.paths != before.paths;
|
||||
}
|
||||
CHECK(roles.count(erTopSolidInfill) == 1);
|
||||
CHECK(roles.count(erBottomSurface) == 1);
|
||||
// Orca: Guard against a vacuous comparison: changing surface centering must change
|
||||
// the printed pattern, while toggling separated sparse infill must leave it alone.
|
||||
CHECK(changed_paths == change_center);
|
||||
}
|
||||
|
||||
TEST_CASE("Separated infill keeps fragmented and nested bodies independent", "[PrintObject][SurfaceInfill][Regression]")
|
||||
{
|
||||
constexpr size_t grid_size = 8;
|
||||
TriangleMesh mesh;
|
||||
auto add_box = [&](double x, double y, double width, double depth) {
|
||||
TriangleMesh box = make_cube(width, depth, 0.6);
|
||||
box.translate(x, y, 0);
|
||||
mesh.merge(box);
|
||||
};
|
||||
// Orca: Many small islands exercise spatial pruning and the tree's original
|
||||
// island indices. A pillar inside a frame also overlaps its bounding box,
|
||||
// but must remain a separate body because it lies entirely inside the hole.
|
||||
for (size_t x = 0; x < grid_size; ++ x)
|
||||
for (size_t y = 0; y < grid_size; ++ y)
|
||||
add_box(6 * x, 6 * y, 3, 3);
|
||||
add_box(54, 0, 20, 4);
|
||||
add_box(54, 16, 20, 4);
|
||||
add_box(54, 0, 4, 20);
|
||||
add_box(70, 0, 4, 20);
|
||||
add_box(62, 8, 4, 4);
|
||||
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({{"separated_infills", true},
|
||||
{"center_of_surface_pattern", "each_surface"},
|
||||
{"layer_height", 0.2},
|
||||
{"initial_layer_print_height", 0.2},
|
||||
{"elefant_foot_compensation", 0},
|
||||
{"wall_loops", 1}});
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({mesh}, print, model, config, nullptr, false);
|
||||
// Orca: Prepare body bounds through the public pipeline, then inspect the object read-only.
|
||||
print.process();
|
||||
const PrintObject &object = *print.objects().front();
|
||||
REQUIRE(object.layer_count() > 1);
|
||||
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());
|
||||
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 &island = layer->lslices_bboxes[i];
|
||||
CHECK(body.min == island.min);
|
||||
CHECK(body.max == island.max);
|
||||
holes += layer->lslices[i].holes.size();
|
||||
}
|
||||
CHECK(holes == 1);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("Body centering survives islands merging and splitting between layers", "[PrintObject][SurfaceInfill][Regression]")
|
||||
{
|
||||
const bool separated = GENERATE(false, true);
|
||||
CAPTURE(separated);
|
||||
// Orca: Four posts join through horizontal then vertical rails, creating a
|
||||
// cycle of overlaps before splitting into four islands again. This exercises
|
||||
// redundant connections and indexing either adjacent layer. A fifth post
|
||||
// stays separate at every height.
|
||||
TriangleMesh mesh;
|
||||
for (int x : {0, 8})
|
||||
for (int y : {0, 8}) {
|
||||
TriangleMesh post = make_cube(4, 4, 1);
|
||||
post.translate(x, y, 0);
|
||||
mesh.merge(post);
|
||||
}
|
||||
for (int y : {0, 8}) {
|
||||
TriangleMesh rail = make_cube(12, 4, 0.2);
|
||||
rail.translate(0, y, 0.2);
|
||||
mesh.merge(rail);
|
||||
}
|
||||
for (int x : {0, 8}) {
|
||||
TriangleMesh rail = make_cube(4, 12, 0.2);
|
||||
rail.translate(x, 0, 0.4);
|
||||
mesh.merge(rail);
|
||||
}
|
||||
TriangleMesh isolated = make_cube(4, 4, 1);
|
||||
isolated.translate(20, 0, 0);
|
||||
mesh.merge(isolated);
|
||||
|
||||
auto config = DynamicPrintConfig::full_print_config();
|
||||
config.set_deserialize_strict({{"separated_infills", separated},
|
||||
{"center_of_surface_pattern", separated ? "each_surface" : "each_model"},
|
||||
{"layer_height", 0.2},
|
||||
{"initial_layer_print_height", 0.2},
|
||||
{"elefant_foot_compensation", 0},
|
||||
{"wall_loops", 1}});
|
||||
Print print;
|
||||
Model model;
|
||||
init_print({mesh}, print, model, config, nullptr, false);
|
||||
// Orca: Prepare body bounds through the public pipeline, then inspect the object read-only.
|
||||
print.process();
|
||||
const PrintObject &object = *print.objects().front();
|
||||
REQUIRE(object.layer_count() == 5);
|
||||
REQUIRE(object.get_layer(0)->lslices.size() == 5);
|
||||
REQUIRE(object.get_layer(1)->lslices.size() == 3);
|
||||
REQUIRE(object.get_layer(2)->lslices.size() == 3);
|
||||
REQUIRE(object.get_layer(4)->lslices.size() == 5);
|
||||
|
||||
BoundingBox isolated_bbox = object.get_layer(0)->lslices_bboxes.front();
|
||||
for (const BoundingBox &bbox : object.get_layer(0)->lslices_bboxes)
|
||||
if (bbox.min.x() > isolated_bbox.min.x())
|
||||
isolated_bbox = bbox;
|
||||
BoundingBox connected_bbox;
|
||||
for (const Layer *layer : object.layers())
|
||||
for (const BoundingBox &bbox : layer->lslices_bboxes)
|
||||
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());
|
||||
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];
|
||||
CHECK(actual.min == expected.min);
|
||||
CHECK(actual.max == expected.max);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user