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

@@ -17,12 +17,12 @@ static const double slope_inner_outer_wall_gap = 0.4;
void ExtrusionPath::intersect_expolygons(const ExPolygons &collection, ExtrusionEntityCollection* retval) const
{
this->_inflate_collection(intersection_pl(Polylines{ polyline }, collection), retval);
this->_inflate_collection(intersection_pl(Polylines{ polyline.to_polyline() }, collection), retval);
}
void ExtrusionPath::subtract_expolygons(const ExPolygons &collection, ExtrusionEntityCollection* retval) const
{
this->_inflate_collection(diff_pl(Polylines{ this->polyline }, collection), retval);
this->_inflate_collection(diff_pl(Polylines{ this->polyline.to_polyline() }, collection), retval);
}
void ExtrusionPath::clip_end(double distance)
@@ -32,11 +32,17 @@ void ExtrusionPath::clip_end(double distance)
void ExtrusionPath::simplify(double tolerance)
{
if (this->z_contoured) {
return;
}
this->polyline.simplify(tolerance);
}
void ExtrusionPath::simplify_by_fitting_arc(double tolerance)
{
if (this->z_contoured) {
return;
}
this->polyline.simplify_by_fitting_arc(tolerance);
}
@@ -45,15 +51,23 @@ double ExtrusionPath::length() const
return this->polyline.length();
}
void ExtrusionPath::collect_points(Points &dst) const
{
dst.reserve(dst.size() + this->polyline.points.size());
for (const Point3 &point : this->polyline.points) {
dst.emplace_back(point.x(), point.y());
}
}
void ExtrusionPath::_inflate_collection(const Polylines &polylines, ExtrusionEntityCollection* collection) const
{
for (const Polyline &polyline : polylines)
collection->entities.emplace_back(new ExtrusionPath(polyline, *this));
collection->entities.emplace_back(new ExtrusionPath(Polyline3(polyline), *this));
}
void ExtrusionPath::polygons_covered_by_width(Polygons &out, const float scaled_epsilon) const
{
polygons_append(out, offset(this->polyline, float(scale_(this->width/2)) + scaled_epsilon));
polygons_append(out, offset(this->polyline.to_polyline(), float(scale_(this->width/2)) + scaled_epsilon));
}
void ExtrusionPath::polygons_covered_by_spacing(Polygons &out, const float scaled_epsilon) const
@@ -64,7 +78,7 @@ void ExtrusionPath::polygons_covered_by_spacing(Polygons &out, const float scale
// SoftFever: TODO Mac trigger assersion errors
// assert(! bridge || this->width == this->height);
auto flow = bridge ? Flow::bridging_flow(this->width, 0.f) : Flow(this->width, this->height, 0.f);
polygons_append(out, offset(this->polyline, 0.5f * float(flow.scaled_spacing()) + scaled_epsilon));
polygons_append(out, offset(this->polyline.to_polyline(), 0.5f * float(flow.scaled_spacing()) + scaled_epsilon));
}
void ExtrusionMultiPath::reverse()
@@ -116,9 +130,10 @@ Polyline ExtrusionMultiPath::as_polyline() const
len -= paths.size() - 1;
assert(len > 0);
out.points.reserve(len);
out.points.push_back(paths.front().polyline.points.front());
out.points.push_back(paths.front().polyline.points.front().to_point());
for (size_t i_path = 0; i_path < paths.size(); ++ i_path)
out.points.insert(out.points.end(), paths[i_path].polyline.points.begin() + 1, paths[i_path].polyline.points.end());
for (auto it = paths[i_path].polyline.points.begin() + 1; it != paths[i_path].polyline.points.end(); ++it)
out.points.push_back(it->to_point());
}
return out;
}
@@ -149,7 +164,9 @@ Polygon ExtrusionLoop::polygon() const
Polygon polygon;
for (const ExtrusionPath &path : this->paths) {
// for each polyline, append all points except the last one (because it coincides with the first one of the next polyline)
polygon.points.insert(polygon.points.end(), path.polyline.points.begin(), path.polyline.points.end()-1);
for (auto it = path.polyline.points.begin(); it != path.polyline.points.end() - 1; ++it) {
polygon.points.push_back(it->to_point());
}
}
return polygon;
}
@@ -168,7 +185,7 @@ bool ExtrusionLoop::split_at_vertex(const Point &point, const double scaled_epsi
if (int idx = path->polyline.find_point(point, scaled_epsilon); idx != -1) {
if (this->paths.size() == 1) {
// just change the order of points
Polyline p1, p2;
Polyline3 p1, p2;
path->polyline.split_at_index(idx, &p1, &p2);
if (p1.is_valid() && p2.is_valid()) {
p2.append(std::move(p1));
@@ -178,7 +195,7 @@ bool ExtrusionLoop::split_at_vertex(const Point &point, const double scaled_epsi
} else {
// new paths list starts with the second half of current path
ExtrusionPaths new_paths;
Polyline p1, p2;
Polyline3 p1, p2;
path->polyline.split_at_index(idx, &p1, &p2);
new_paths.reserve(this->paths.size() + 1);
{
@@ -218,16 +235,17 @@ ExtrusionLoop::ClosestPathPoint ExtrusionLoop::get_closest_path_and_point(const
ClosestPathPoint best_non_overhang{0, 0};
double min2_non_overhang = std::numeric_limits<double>::max();
for (const ExtrusionPath &path : this->paths) {
std::pair<int, Point> foot_pt_ = foot_pt(path.polyline.points, point);
double d2 = (foot_pt_.second - point).cast<double>().squaredNorm();
std::pair<int, Point3> foot_pt_ = foot_pt(path.polyline.points, Point3(point));
Point foot_pt_2d = Point(foot_pt_.second.x(), foot_pt_.second.y());
double d2 = (foot_pt_2d - point).cast<double>().squaredNorm();
if (d2 < min2) {
out.foot_pt = foot_pt_.second;
out.foot_pt = foot_pt_2d;
out.path_idx = &path - &this->paths.front();
out.segment_idx = foot_pt_.first;
min2 = d2;
}
if (prefer_non_overhang && !is_bridge(path.role()) && d2 < min2_non_overhang) {
best_non_overhang.foot_pt = foot_pt_.second;
best_non_overhang.foot_pt = foot_pt_2d;
best_non_overhang.path_idx = &path - &this->paths.front();
best_non_overhang.segment_idx = foot_pt_.first;
min2_non_overhang = d2;
@@ -249,16 +267,18 @@ void ExtrusionLoop::split_at(const Point &point, bool prefer_non_overhang, const
// Snap p to start or end of segment_idx if closer than scaled_epsilon.
{
const Point *p1 = this->paths[path_idx].polyline.points.data() + segment_idx;
const Point *p2 = p1;
const Point3 *p1 = this->paths[path_idx].polyline.points.data() + segment_idx;
const Point3 *p2 = p1;
++p2;
double d2_1 = (point - *p1).cast<double>().squaredNorm();
double d2_2 = (point - *p2).cast<double>().squaredNorm();
Point p1_2d = Point(p1->x(), p1->y());
Point p2_2d = Point(p2->x(), p2->y());
double d2_1 = (point - p1_2d).cast<double>().squaredNorm();
double d2_2 = (point - p2_2d).cast<double>().squaredNorm();
const double thr2 = scaled_epsilon * scaled_epsilon;
if (d2_1 < d2_2) {
if (d2_1 < thr2) p = *p1;
if (d2_1 < thr2) p = p1_2d;
} else {
if (d2_2 < thr2) p = *p2;
if (d2_2 < thr2) p = p2_2d;
}
}
@@ -411,16 +431,16 @@ ExtrusionLoopSloped::ExtrusionLoopSloped(ExtrusionPaths& original_paths,
: ExtrusionLoop(role)
{
// create slopes
const auto add_slop = [this, slope_max_segment_length, seam_gap](const ExtrusionPath &path, const Polyline &poly, double ratio_begin, double ratio_end) {
const auto add_slop = [this, slope_max_segment_length, seam_gap](const ExtrusionPath &path, const Polyline3 &poly, double ratio_begin, double ratio_end) {
if (poly.empty()) { return; }
// Ensure `slope_max_segment_length`
Polyline detailed_poly;
Polyline3 detailed_poly;
{
detailed_poly.append(poly.first_point());
// Recursively split the line into half until no longer than `slope_max_segment_length`
const std::function<void(const Line &)> handle_line = [slope_max_segment_length, &detailed_poly, &handle_line](const Line &line) {
const std::function<void(const Line3 &)> handle_line = [slope_max_segment_length, &detailed_poly, &handle_line](const Line3 &line) {
if (line.length() <= slope_max_segment_length) {
detailed_poly.append(line.b);
} else {
@@ -441,8 +461,8 @@ ExtrusionLoopSloped::ExtrusionLoopSloped(ExtrusionPaths& original_paths,
const auto seg_length = detailed_poly.length();
if (seg_length > seam_gap) {
// Split the segment and remove the last `seam_gap` bit
const Polyline orig = detailed_poly;
Polyline tmp;
const Polyline3 orig = detailed_poly;
Polyline3 tmp;
orig.split_at_length(seg_length - seam_gap, &detailed_poly, &tmp);
ratio_end = lerp(ratio_begin, ratio_end, (seg_length - seam_gap) / seg_length);
@@ -464,8 +484,8 @@ ExtrusionLoopSloped::ExtrusionLoopSloped(ExtrusionPaths& original_paths,
const double path_len = unscale_(path->length());
if (path_len > remaining_length) {
// Split current path into slope and non-slope part
Polyline slope_path;
Polyline flat_path;
Polyline3 slope_path;
Polyline3 flat_path;
path->polyline.split_at_length(scale_(remaining_length), &slope_path, &flat_path);
add_slop(*path, slope_path, start_ratio, 1);
@@ -631,4 +651,28 @@ ExtrusionRole ExtrusionEntity::string_to_role(const std::string_view role)
return erNone;
}
// ExtrusionPathContoured implementation
ExtrusionEntity *ExtrusionPathContoured::clone() const {
return new ExtrusionPathContoured(*this);
}
ExtrusionEntity *ExtrusionPathContoured::clone_move() {
return new ExtrusionPathContoured(std::move(*this));
}
void ExtrusionPathContoured::simplify(double tolerance) {
// Do not simplify contoured paths
return;
}
void ExtrusionPathContoured::simplify_by_fitting_arc(double tolerance) {
// Do not simplify contoured paths
return;
}
void ExtrusionPathContoured::reverse() {
this->polyline.reverse();
std::reverse(this->z_diffs.begin(), this->z_diffs.end());
}
}