Fix: Only one wall top surfaces (#14929)

This commit is contained in:
Ian Bassi
2026-07-29 09:23:35 -03:00
committed by GitHub
parent 29d4513694
commit 6489b4cad3
5 changed files with 583 additions and 73 deletions

View File

@@ -360,6 +360,14 @@ static ClipperLib_Z::Paths clip_extrusion(const ClipperLib_Z::Path& subject, con
return clipped_paths;
}
static double clipper_z_path_length(const ClipperLib_Z::Path &path)
{
double len = 0.;
for (size_t i = 1; i < path.size(); ++ i)
len += (Vec2d(double(path[i].x()), double(path[i].y())) - Vec2d(double(path[i - 1].x()), double(path[i - 1].y()))).norm();
return len;
}
struct PerimeterGeneratorArachneExtrusion
{
Arachne::ExtrusionLine* extrusion = nullptr;
@@ -571,17 +579,156 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
return extrusion_coll;
}
// ORCA: only_one_wall_top detects the top as "slice upper", so a feature rising from the middle of a
// top surface becomes an enclosed hole that gets ringed with extra inner walls. Fill those holes back
// into the top. Only holes that are both covered by the upper layer (excludes bridges) and backed by
// solid material (excludes voids) are filled.
static ExPolygons fill_enclosed_top_feature_holes(const ExPolygons &top, const Polygons &covered_by_upper, const ExPolygons &solid)
// ORCA: only_one_wall_top acts on top surfaces, so without a top shell there is nothing for it to act on: zero top
// shell layers retype the top surfaces as internal, see LayerRegion::prepare_fill_surfaces(). A 0% top surface
// density does leave a top surface - just an unfilled one - so it does not disable the feature.
// ConfigManipulation::toggle_print_fff_options() hides the option under the same condition, so a profile that left
// it enabled does not act behind a hidden checkbox.
static bool has_top_shell_layers(const PrintRegionConfig &config)
{
ExPolygons filled = top;
for (ExPolygon &ex : filled)
ex.holes.clear();
const ExPolygons feature_holes = intersection_ex(intersection_ex(diff_ex(filled, top), covered_by_upper), solid);
return feature_holes.empty() ? top : union_ex(top, feature_holes);
return config.top_shell_layers.value > 0;
}
// ORCA: only_one_wall_first_layer thins the first layer to a single wall, the bottom counterpart of the above and
// gated the same way: zero bottom shell layers retype the bottom surfaces as internal, so that wall would ring
// sparse infill on the bed. The bottom surface density plays no part - an unfilled bottom surface is still a bottom
// surface, exactly as for the top - and it cannot reach zero anyway, being capped at a 10% minimum.
static bool has_bottom_shell_layers(const PrintRegionConfig &config)
{
return config.bottom_shell_layers.value > 0;
}
// ORCA: the inner walls are only given up when a top fill takes their space, and it has to actually reach it -
// a 0% top surface density leaves no fill at all, and without top_surface_expansion the fill never grows over
// them. Either way the original generation is kept (re-onion the not-top region), which is what users of
// only_one_wall_top alone have always got.
static bool top_fill_replaces_inner_walls(const PrintRegionConfig &config)
{
return has_top_shell_layers(config) && config.top_surface_density.value > 0 && config.top_surface_expansion.value > 0;
}
// ORCA: only_one_wall_top - cheap per-vertex classification of a wall against the top surface. Only Partial
// needs the geometry clipped or measured; a segment crossing the top with no vertex inside is rare enough to ignore.
enum class TopOverlap { None, Partial, Full };
static bool point_over_top(const Point &p, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
if (! top_region_bbox.contains(p))
return false;
for (const ExPolygon &ex : top_region)
if (ex.contains(p, false))
return true;
return false;
}
static TopOverlap classify_over_top(const Points &pts, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Point &p : pts)
if (point_over_top(p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == pts.size() ? TopOverlap::Full : TopOverlap::Partial;
}
static TopOverlap classify_over_top(const Arachne::ExtrusionLine &el, const ExPolygons &top_region, const BoundingBox &top_region_bbox)
{
size_t inside = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions)
if (point_over_top(j.p, top_region, top_region_bbox))
++ inside;
return inside == 0 ? TopOverlap::None : inside == el.junctions.size() ? TopOverlap::Full : TopOverlap::Partial;
}
// ORCA: only_one_wall_top for Arachne - cut out of the already generated inner walls the parts running over the top
// surface, so geometry that continues upward keeps its walls. A wall too short over the top to be worth slitting open
// is left whole, its footprint reported in kept_over_top for the caller to withhold from the top fill.
static void clip_inner_walls_over_top(std::vector<Arachne::VariableWidthLines> &inner_perimeters, const ExPolygons &top_region, coord_t perimeter_width, Polygons &kept_over_top)
{
const BoundingBox top_region_bbox = get_extents(top_region).inflated(SCALED_EPSILON);
auto covered_by = [](const Arachne::ExtrusionLine &el) {
Polyline centerline;
centerline.points.reserve(el.junctions.size());
coord_t width = 0;
for (const Arachne::ExtrusionJunction &j : el.junctions) {
centerline.points.emplace_back(j.p);
width = std::max(width, j.w);
}
return offset(centerline, float(width) / 2.f);
};
// Pull the cut back by half a wall width: the clip severs the centerline, but the bead's rounded end
// extends half a width past its endpoint and would otherwise overlap the top fill.
ClipperLib_Z::Paths top_paths_z;
for (const Polygon &poly : to_polygons(offset_ex(top_region, float(perimeter_width) / 2.f))) {
top_paths_z.emplace_back();
ClipperLib_Z::Path &out = top_paths_z.back();
out.reserve(poly.points.size());
for (const Point &pt : poly.points)
out.emplace_back(pt.x(), pt.y(), 0);
}
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
Arachne::VariableWidthLines kept;
kept.reserve(inner_perimeter.size());
for (Arachne::ExtrusionLine &el : inner_perimeter) {
if (el.empty())
continue;
const TopOverlap overlap = classify_over_top(el, top_region, top_region_bbox);
if (overlap == TopOverlap::None) {
kept.emplace_back(std::move(el));
continue;
}
if (overlap == TopOverlap::Full)
continue; // the clip below would return nothing anyway
ClipperLib_Z::Path subject;
subject.reserve(el.size());
for (const Arachne::ExtrusionJunction &j : el.junctions)
subject.emplace_back(j.p.x(), j.p.y(), j.w);
ClipperLib_Z::Paths pieces = clip_extrusion(subject, top_paths_z, ClipperLib_Z::ctDifference);
// Clipper treats the subject as an open polyline, so it also cuts a closed loop at its (arbitrary)
// start vertex and may reverse pieces. Stitch pieces sharing an endpoint back together.
auto same_pt = [](const ClipperLib_Z::IntPoint &p, const ClipperLib_Z::IntPoint &q) {
return std::abs(p.x() - q.x()) <= SCALED_EPSILON && std::abs(p.y() - q.y()) <= SCALED_EPSILON;
};
for (size_t i = 0; i < pieces.size(); ++ i) {
for (size_t j = i + 1; j < pieces.size();) {
ClipperLib_Z::Path &a = pieces[i];
ClipperLib_Z::Path &b = pieces[j];
if (same_pt(a.front(), b.front()) || same_pt(a.front(), b.back()))
std::reverse(a.begin(), a.end());
if (same_pt(a.back(), b.back()))
std::reverse(b.begin(), b.end());
if (same_pt(a.back(), b.front())) {
a.insert(a.end(), b.begin() + 1, b.end());
pieces.erase(pieces.begin() + j);
j = i + 1; // the merged path has new endpoints, restart the scan
} else
++ j;
}
}
// If the clip removed next to nothing, keep the loop untouched instead of slitting it open. The
// half-width pull-back above already costs about one width per crossing, hence two widths.
double kept_length = 0.;
for (const ClipperLib_Z::Path &path : pieces)
kept_length += clipper_z_path_length(path);
if (clipper_z_path_length(subject) - kept_length < 2. * double(perimeter_width)) {
append(kept_over_top, covered_by(el));
kept.emplace_back(std::move(el));
continue;
}
for (const ClipperLib_Z::Path &path : pieces) {
Arachne::ExtrusionLine clipped(el.inset_idx, el.is_odd);
clipped.junctions.reserve(path.size());
for (const ClipperLib_Z::IntPoint &pt : path)
clipped.junctions.emplace_back(Point(pt.x(), pt.y()), coord_t(pt.z()), el.inset_idx);
// Discard tiny leftovers that would print as zits.
if (clipped.size() >= 2 && clipped.getLength() >= perimeter_width)
kept.emplace_back(std::move(clipped));
}
}
inner_perimeter = std::move(kept);
}
}
void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExPolygons &top_fills,
@@ -649,7 +796,6 @@ void PerimeterGenerator::split_top_surfaces(const ExPolygons &orig_polygons, ExP
ExPolygons delete_bridge = diff_ex(orig_polygons, bridge_checker, ApplySafetyOffset::Yes);
ExPolygons top_polygons = diff_ex(delete_bridge, upper_polygons_series_clipped, ApplySafetyOffset::Yes);
top_polygons = fill_enclosed_top_feature_holes(top_polygons, upper_polygons_series_clipped, orig_polygons);
// get the not-top surface, from the "real top" but enlarged by external_infill_margin (and the
// min_width_top_surface we removed a bit before)
@@ -1234,6 +1380,11 @@ void PerimeterGenerator::process_classic()
for (const Surface &surface : all_surfaces)
surface_exp.push_back(surface.expolygon);
std::vector<size_t> surface_order = chain_expolygons(surface_exp);
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
for (size_t order_idx = 0; order_idx < surface_order.size(); order_idx++) {
const Surface &surface = all_surfaces[surface_order[order_idx]];
// detect how many perimeters must be generated for this island
@@ -1241,16 +1392,23 @@ void PerimeterGenerator::process_classic()
int sparse_infill_density = this->config->sparse_infill_density.value;
if (this->config->alternate_extra_wall && this->layer_id % 2 == 1 && !m_spiral_vase && sparse_infill_density > 0) // add alternating extra wall
loop_number++;
if (this->layer_id == object_config->raft_layers && this->config->only_one_wall_first_layer)
if (this->layer_id == object_config->raft_layers && only_one_wall_first_layer)
loop_number = 0;
// Set the topmost layer to be one wall
if (loop_number > 0 && config->only_one_wall_top && this->upper_slices == nullptr)
if (loop_number > 0 && only_one_wall_top && this->upper_slices == nullptr)
loop_number = 0;
ExPolygons last = union_ex(surface.expolygon.simplify_p(surface_simplify_resolution));
ExPolygons gaps;
ExPolygons top_fills;
ExPolygons fill_clip;
// ORCA: only_one_wall_top, all empty unless this island has a top surface on this layer. See the
// post-onion reduction below: the region to keep clear of inner walls, the space freed by the dropped
// walls (goes to infill, not left as a void) and the space held by the kept ones (withheld from the fill).
ExPolygons one_wall_top_region;
ExPolygons one_wall_top_reclaimed;
Polygons one_wall_top_kept_bands;
bool apply_one_wall_top = false;
if (loop_number >= 0) {
// In case no perimeters are to be generated, loop_number will equal to -1.
std::vector<PerimeterGeneratorLoops> contours(loop_number+1); // depth => loops
@@ -1389,8 +1547,19 @@ void PerimeterGenerator::process_classic()
//BBS: refer to superslicer
//store surface for top infill if only_one_wall_top
if (i == 0 && i!=loop_number && config->only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
this->split_top_surfaces(last, top_fills, last, fill_clip);
if (i == 0 && i!=loop_number && only_one_wall_top && !surface.is_bridge() && this->upper_slices != NULL) {
if (top_fill_replaces_inner_walls(*this->config)) {
// ORCA: take the top fill and the keep-out region but leave `last` as the real geometry,
// so the onion follows it and the walls over the top are reduced in one step below.
ExPolygons non_top_polygons;
this->split_top_surfaces(last, top_fills, non_top_polygons, fill_clip);
apply_one_wall_top = !top_fills.empty();
if (apply_one_wall_top)
one_wall_top_region = diff_ex(last, non_top_polygons);
} else {
// Onion the not-top region only, so the remaining walls stop at the top boundary.
this->split_top_surfaces(last, top_fills, last, fill_clip);
}
}
if (i == loop_number && (! has_gap_fill || this->config->sparse_infill_density.value == 0)) {
@@ -1400,6 +1569,46 @@ void PerimeterGenerator::process_classic()
}
}
// ORCA: only_one_wall_top reduction - drop the inner walls (depth > 0) running over the top surface and
// take that space back from the gaps, leaving the top with the outer wall and the top infill. Classic
// perimeters are closed loops, so a wall can only be kept or dropped whole; one that merely grazes the
// top (same tolerance as the Arachne clip) is kept and withheld from the top fill instead.
if (apply_one_wall_top) {
const BoundingBox top_region_bbox = get_extents(one_wall_top_region).inflated(SCALED_EPSILON);
const double grazing_tolerance = 2. * double(perimeter_width);
// The band a wall covers, taken around its centerline so the orientation of holes does not matter.
auto wall_band = [perimeter_spacing](const Polygon &poly) {
Polygon centerline = poly;
centerline.make_counter_clockwise();
return diff(offset(centerline, float(perimeter_spacing) / 2.f),
offset(centerline, -float(perimeter_spacing) / 2.f));
};
Polygons dropped_wall_bands;
auto reduce_over_top = [&](PerimeterGeneratorLoops &loops) {
loops.erase(std::remove_if(loops.begin(), loops.end(), [&](const PerimeterGeneratorLoop &loop) {
const TopOverlap overlap = classify_over_top(loop.polygon.points, one_wall_top_region, top_region_bbox);
if (overlap == TopOverlap::None)
return false;
// Only a wall straddling the boundary is worth measuring; a wall wholly over the top goes.
if (overlap == TopOverlap::Partial &&
total_length(intersection_pl(Polylines{ loop.polygon.split_at_first_point() }, one_wall_top_region)) < grazing_tolerance) {
append(one_wall_top_kept_bands, wall_band(loop.polygon));
return false;
}
append(dropped_wall_bands, wall_band(loop.polygon));
return true;
}), loops.end());
};
for (int d = 1; d <= loop_number; ++ d) {
reduce_over_top(contours[d]);
reduce_over_top(holes[d]);
}
if (! gaps.empty())
gaps = diff_ex(gaps, one_wall_top_region);
if (! dropped_wall_bands.empty())
one_wall_top_reclaimed = diff_ex(dropped_wall_bands, one_wall_top_region);
}
// nest loops: holes first
for (int d = 0; d <= loop_number; ++ d) {
PerimeterGeneratorLoops &holes_d = holes[d];
@@ -1634,7 +1843,10 @@ void PerimeterGenerator::process_classic()
and use zigzag). */
//FIXME Vojtech: This grows by a rounded extrusion width, not by line spacing,
// therefore it may cover the area, but no the volume.
last = diff_ex(last, gap_fill.polygons_covered_by_width(10.f));
Polygons gap_fill_covered = gap_fill.polygons_covered_by_width(10.f);
last = diff_ex(last, gap_fill_covered);
if (! one_wall_top_reclaimed.empty())
one_wall_top_reclaimed = diff_ex(one_wall_top_reclaimed, gap_fill_covered);
this->gap_fill->append(std::move(gap_fill.entities));
}
@@ -1679,9 +1891,15 @@ void PerimeterGenerator::process_classic()
// append infill areas to fill_surfaces
//if any top_fills, grow them by ext_perimeter_spacing/2 to have the real un-anchored fill
ExPolygons top_infill_exp = intersection_ex(fill_clip, offset_ex(top_fills, double(ext_perimeter_spacing / 2)));
// ORCA: only_one_wall_top - route the top fill around the walls kept despite grazing the top.
if (!one_wall_top_kept_bands.empty())
top_infill_exp = diff_ex(top_infill_exp, one_wall_top_kept_bands);
if (!top_fills.empty()) {
infill_exp = union_ex(infill_exp, offset_ex(top_infill_exp, double(top_infill_peri_overlap)));
}
// ORCA: only_one_wall_top - what the top fill does not cover of the dropped walls goes to infill.
if (!one_wall_top_reclaimed.empty())
infill_exp = union_ex(infill_exp, one_wall_top_reclaimed);
this->fill_surfaces->append(infill_exp, stInternal);
apply_extra_perimeters(infill_exp);
@@ -1700,6 +1918,8 @@ void PerimeterGenerator::process_classic()
double(-inset - infill_peri_overlap));
if (!top_fills.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, top_infill_exp);
if (!one_wall_top_reclaimed.empty())
polyWithoutOverlap = union_ex(polyWithoutOverlap, one_wall_top_reclaimed);
this->fill_no_overlap->insert(this->fill_no_overlap->end(), polyWithoutOverlap.begin(), polyWithoutOverlap.end());
}
@@ -2138,6 +2358,11 @@ void PerimeterGenerator::process_arachne()
process_no_bridge(all_surfaces, perimeter_spacing, ext_perimeter_width);
// BBS: don't simplify too much which influence arc fitting when export gcode if arc_fitting is enabled
double surface_simplify_resolution = (print_config->enable_arc_fitting && !this->has_fuzzy_skin) ? 0.2 * m_scaled_resolution : m_scaled_resolution;
// ORCA: neither one-wall option has a surface to act on without the shell behind it, see
// has_top_shell_layers() / has_bottom_shell_layers(). Gated here so every use below - including the
// topmost and first layers - sees the same answer.
const bool only_one_wall_top = this->config->only_one_wall_top && has_top_shell_layers(*this->config);
const bool only_one_wall_first_layer = this->config->only_one_wall_first_layer && has_bottom_shell_layers(*this->config);
// we need to process each island separately because we might have different
// extra perimeters for each one
for (const Surface& surface : all_surfaces) {
@@ -2150,12 +2375,12 @@ void PerimeterGenerator::process_arachne()
// Set the bottommost layer to be one wall
const bool is_bottom_layer = (this->layer_id == object_config->raft_layers) ? true : false;
if (is_bottom_layer && this->config->only_one_wall_first_layer)
if (is_bottom_layer && only_one_wall_first_layer)
loop_number = 0;
// Orca: set the topmost layer to be one wall according to the config
const bool is_topmost_layer = (this->upper_slices == nullptr) ? true : false;
if (is_topmost_layer && loop_number > 0 && config->only_one_wall_top)
if (is_topmost_layer && loop_number > 0 && only_one_wall_top)
loop_number = 0;
auto apply_precise_outer_wall = config->precise_outer_wall && config->wall_sequence == WallSequence::InnerOuter;
@@ -2175,10 +2400,10 @@ void PerimeterGenerator::process_arachne()
//PS: One wall top surface for Arachne
ExPolygons top_expolygons;
// Calculate how many inner loops remain when TopSurfaces is selected.
const int inner_loop_number = (config->only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
const int inner_loop_number = (only_one_wall_top && upper_slices != nullptr) ? loop_number - 1 : -1;
// Set one perimeter when TopSurfaces is selected.
if (config->only_one_wall_top && loop_number > 0)
if (only_one_wall_top && loop_number > 0)
loop_number = 0;
Arachne::WallToolPathsParams input_params_tmp = input_params;
@@ -2209,7 +2434,6 @@ void PerimeterGenerator::process_arachne()
upper_slices_clipped = ClipperUtils::clip_clipper_polygons_with_subject_bbox(*upper_slices, infill_contour_bbox);
top_expolygons = diff_ex(infill_contour, upper_slices_clipped);
top_expolygons = fill_enclosed_top_feature_holes(top_expolygons, upper_slices_clipped, infill_contour);
if (!top_expolygons.empty()) {
if (lower_slices != nullptr) {
@@ -2230,25 +2454,33 @@ void PerimeterGenerator::process_arachne()
// due to thin lines being generated
top_expolygons = offset2_ex(top_expolygons, -top_surface_min_width, top_surface_min_width + float(perimeter_width * 0.85));
// Get the not-top ExPolygons (including bridges) from current slices and expanded real top ExPolygons (without bridges).
const ExPolygons not_top_expolygons = diff_ex(infill_contour, top_expolygons);
// Get final top ExPolygons.
// Get final top ExPolygons (bridges were excluded above, so they stay walled).
top_expolygons = intersection_ex(top_expolygons, infill_contour);
const Polygons not_top_polygons = to_polygons(offset_ex(not_top_expolygons,wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(not_top_polygons, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
// ORCA: onion the real region (inside the outer wall) so the remaining walls follow the actual
// geometry, then cut away the parts over the top surface. Re-onioning the non-top complement
// instead - the fallback when there is no top fill - walls the top/non-top interface and rings
// top-surface islands with inner walls that don't exist when the feature is disabled.
const bool clip_walls_over_top = top_fill_replaces_inner_walls(*this->config);
const Polygons inner_region = to_polygons(offset_ex(clip_walls_over_top ? infill_contour
: diff_ex(infill_contour, top_expolygons),
wall_0_inset));
Arachne::WallToolPaths inner_wall_tool_paths(inner_region, perimeter_spacing, perimeter_spacing, coord_t(inner_loop_number + 1), 0, layer_height, input_params_tmp);
std::vector<Arachne::VariableWidthLines> inner_perimeters = inner_wall_tool_paths.getToolPaths();
// Recalculate indexes of inner perimeters before merging them.
if (!perimeters.empty()) {
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters) {
if (inner_perimeter.empty())
continue;
if (clip_walls_over_top) {
Polygons kept_over_top;
clip_inner_walls_over_top(inner_perimeters, top_expolygons, perimeter_width, kept_over_top);
// Route the top fill around the walls kept despite grazing the top.
if (! kept_over_top.empty())
top_expolygons = diff_ex(top_expolygons, kept_over_top);
}
// Recalculate indexes of inner perimeters before merging them: they come after the single outer wall.
if (!perimeters.empty())
for (Arachne::VariableWidthLines &inner_perimeter : inner_perimeters)
for (Arachne::ExtrusionLine &el : inner_perimeter)
++el.inset_idx;
}
}
perimeters.insert(perimeters.end(), inner_perimeters.begin(), inner_perimeters.end());
infill_contour = union_ex(top_expolygons, inner_wall_tool_paths.getInnerContour());