feat: Add Z Anti-Aliasing (ZAA) contouring support

Port Z Anti-Aliasing from BambuStudio-ZAA (https://github.com/adob/BambuStudio-ZAA)
to OrcaSlicer. ZAA eliminates stair-stepping on curved and sloped top surfaces
by raycasting each extrusion point against the original 3D mesh and micro-adjusting
Z height to follow the actual surface geometry.

Key changes:
- Add ContourZ.cpp raycasting algorithm (~330 lines)
- Extend geometry with 3D support (Point3, Line3, Polyline3, MultiPoint3)
- Template arc fitting for 2D/3D compatibility
- Change ExtrusionPath::polyline from Polyline to Polyline3
- Add 5 ZAA config options (zaa_enabled, zaa_min_z, etc.)
- Add posContouring pipeline step in PrintObject
- Update GCode writer for 3D coordinate output
- Add ZAA settings UI in Print Settings > Quality
- Add docs/ZAA.md with usage and implementation details

ZAA is opt-in and disabled by default. When disabled, the slicing pipeline
is unchanged.
This commit is contained in:
Matthias Nott
2026-02-09 20:38:46 +01:00
parent cae1567726
commit 963f8d86b7
57 changed files with 1817 additions and 204 deletions

View File

@@ -206,7 +206,8 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
// Reapply the nearest point search for starting point.
// We allow polyline reversal because Clipper may have randomly reversed polylines during clipping.
if(paths.empty()) continue;
chain_and_reorder_extrusion_paths(paths, &paths.front().first_point());
Point start_pt = Point(paths.front().first_point().x(), paths.front().first_point().y());
chain_and_reorder_extrusion_paths(paths, &start_pt);
} else {
if (overhangs_reverse && perimeter_generator.layer_id > perimeter_generator.object_config->raft_layers) {
// Always reverse if detect overhang wall is not enabled
@@ -216,7 +217,7 @@ static ExtrusionEntityCollection traverse_loops(const PerimeterGenerator &perime
ExtrusionPath path(role);
//BBS.
path.polyline = polygon.split_at_first_point();
path.polyline = Polyline3(polygon.split_at_first_point());
path.mm3_per_mm = extrusion_mm3_per_mm;
path.width = extrusion_width;
path.height = (float)perimeter_generator.layer_height;
@@ -429,7 +430,7 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
Polylines be_clipped;
for (const ExtrusionPath &p : it.second) {
be_clipped.emplace_back(std::move(p.polyline));
be_clipped.emplace_back(p.polyline.to_polyline());
}
BoundingBox extrusion_bboxs = get_extents(be_clipped);
@@ -463,11 +464,15 @@ static ExtrusionEntityCollection traverse_extrusions(const PerimeterGenerator& p
};
std::unordered_map<Point, PointInfo, PointHash> point_occurrence;
for (const ExtrusionPath& path : paths) {
++point_occurrence[path.polyline.first_point()].occurrence;
++point_occurrence[path.polyline.last_point()].occurrence;
const Point3 &first_p3 = path.polyline.first_point();
const Point3 &last_p3 = path.polyline.last_point();
Point first_p = Point(first_p3.x(), first_p3.y());
Point last_p = Point(last_p3.x(), last_p3.y());
++point_occurrence[first_p].occurrence;
++point_occurrence[last_p].occurrence;
if (path.role() == erOverhangPerimeter) {
point_occurrence[path.polyline.first_point()].is_overhang = true;
point_occurrence[path.polyline.last_point()].is_overhang = true;
point_occurrence[first_p].is_overhang = true;
point_occurrence[last_p].is_overhang = true;
}
}
@@ -655,11 +660,13 @@ bool paths_touch(const ExtrusionPath &path_one, const ExtrusionPath &path_two, d
{
AABBTreeLines::LinesDistancer<Line> lines_two{path_two.as_polyline().lines()};
for (size_t pt_idx = 0; pt_idx < path_one.polyline.size(); pt_idx++) {
if (lines_two.distance_from_lines<false>(path_one.polyline.points[pt_idx]) < limit_distance) { return true; }
const Point3 &p3 = path_one.polyline.points[pt_idx];
if (lines_two.distance_from_lines<false>(Point(p3.x(), p3.y())) < limit_distance) { return true; }
}
AABBTreeLines::LinesDistancer<Line> lines_one{path_one.as_polyline().lines()};
for (size_t pt_idx = 0; pt_idx < path_two.polyline.size(); pt_idx++) {
if (lines_one.distance_from_lines<false>(path_two.polyline.points[pt_idx]) < limit_distance) { return true; }
const Point3 &p3 = path_two.polyline.points[pt_idx];
if (lines_one.distance_from_lines<false>(Point(p3.x(), p3.y())) < limit_distance) { return true; }
}
return false;
}
@@ -1013,7 +1020,7 @@ std::tuple<std::vector<ExtrusionPaths>, Polygons> generate_extra_perimeters_over
// polyline)
bool first_overhang_is_closed_and_anchored =
(overhang_region.front().first_point() == overhang_region.front().last_point() &&
!intersection_pl(overhang_region.front().polyline, optimized_lower_slices).empty());
!intersection_pl(overhang_region.front().polyline.to_polyline(), optimized_lower_slices).empty());
auto is_anchored = [&lower_layer_aabb_tree](const ExtrusionPath &path) {
return lower_layer_aabb_tree.distance_from_lines<true>(path.first_point()) <= 0 ||
@@ -1025,7 +1032,8 @@ std::tuple<std::vector<ExtrusionPaths>, Polygons> generate_extra_perimeters_over
size_t min_dist_idx = 0;
double min_dist = std::numeric_limits<double>::max();
for (size_t i = 0; i < overhang_region.front().polyline.size(); i++) {
Point p = overhang_region.front().polyline[i];
const Point3 &p3 = overhang_region.front().polyline.points[i];
Point p = Point(p3.x(), p3.y());
if (double d = lower_layer_aabb_tree.distance_from_lines<true>(p) < min_dist) {
min_dist = d;
min_dist_idx = i;