Merge pull request #24044 from davidgilkaufman/adaptive_stepover

[CAM] Improve Adaptive operation to successfully generate for small stepover
This commit is contained in:
sliptonic
2025-11-14 11:43:34 -06:00
committed by GitHub
2 changed files with 147 additions and 104 deletions

View File

@@ -29,6 +29,7 @@
#include <ctime>
#include <algorithm>
#include <numbers>
#include <optional>
namespace ClipperLib
{
@@ -1117,72 +1118,84 @@ public:
void clear()
{
angles.clear();
areas.clear();
m_min.reset();
m_max.reset();
}
bool bothSides()
{
return m_min && m_max && m_min->second < 0 && m_max->second >= 0;
}
// adds point keeping the incremental order of areas for interpolation to work correctly
void addPoint(double area, double angle)
void addPoint(double error, std::pair<double, IntPoint> angle, bool allowSkip = false)
{
std::size_t size = areas.size();
if (size == 0 || area > areas[size - 1] + NTOL) { // first point or largest area point
areas.push_back(area);
angles.push_back(angle);
return;
if (!m_min) {
m_min = {angle, error};
}
for (std::size_t i = 0; i < size; i++) {
if (area < areas[i] - NTOL && (i == 0 || area > areas[i - 1] + NTOL)) {
areas.insert(areas.begin() + i, area);
angles.insert(angles.begin() + i, angle);
else if (!m_max) {
m_max = {angle, error};
if (m_min->second > m_max->second) {
auto tmp = m_min;
m_min = m_max;
m_max = tmp;
}
}
else if (bothSides()) {
if (error < 0) {
m_min = {angle, error};
}
else {
m_max = {angle, error};
}
}
else {
if (allowSkip && abs(error) > abs(m_min->second) && abs(error) > abs(m_max->second)) {
return;
}
if (abs(m_min->second) > abs(m_max->second)) {
m_min.reset();
}
else {
m_max.reset();
}
addPoint(error, angle);
}
}
double interpolateAngle(double targetArea)
double interpolateAngle()
{
std::size_t size = areas.size();
if (size < 2 || targetArea > areas[size - 1]) {
return MIN_ANGLE; // max engage angle - convenient value to initially measure cut area
}
if (targetArea < areas[0]) {
return MAX_ANGLE; // min engage angle
if (!m_min) {
return MIN_ANGLE;
}
for (size_t i = 1; i < size; i++) {
// find 2 subsequent points where target area is between
if (areas[i - 1] <= targetArea && areas[i] > targetArea) {
// linear interpolation
double af = (targetArea - areas[i - 1]) / (areas[i] - areas[i - 1]);
double a = angles[i - 1] + af * (angles[i] - angles[i - 1]);
return a;
}
if (!m_max) {
return MAX_ANGLE;
}
return MIN_ANGLE;
double p = (0 - m_min->second) / (m_max->second - m_min->second);
// Ensure search is sufficiently efficient -- this is a compromise
// between binary search (p = 0.5, guaranteed search completion in log
// time) and following linear interpolation completely (often faster
// since area cut is locally linear in movement angle)
const double minInterp = .2;
p = max(min(p, 1 - minInterp), minInterp);
return m_min->first.first * (1 - p) + m_max->first.first * p;
}
double clampAngle(double angle)
{
if (angle < MIN_ANGLE) {
return MIN_ANGLE;
}
if (angle > MAX_ANGLE) {
return MAX_ANGLE;
}
return angle;
return max(min(angle, MAX_ANGLE), MIN_ANGLE);
}
double getRandomAngle()
{
return MIN_ANGLE + (MAX_ANGLE - MIN_ANGLE) * double(rand()) / double(RAND_MAX);
}
size_t getPointCount()
{
return areas.size();
return (m_min ? 1 : 0) + (m_max ? 1 : 0);
}
private:
vector<double> angles;
vector<double> areas;
public:
// {{angle, clipper point}, error}
std::optional<std::pair<std::pair<double, IntPoint>, double>> m_min;
std::optional<std::pair<std::pair<double, IntPoint>, double>> m_max;
};
//***************************************
@@ -1570,7 +1583,7 @@ double Adaptive2d::CalcCutArea(
maxFi += 2 * std::numbers::pi;
}
if (preventConventional && interPathLen >= RESOLUTION_FACTOR) {
if (preventConventional && interPathLen >= MIN_STEP_CLIPPER) {
// detect conventional mode cut - we want only climb mode
IntPoint midPoint(
long(c2.X + toolRadiusScaled * cos(0.5 * (maxFi + minFi))),
@@ -1588,7 +1601,7 @@ double Adaptive2d::CalcCutArea(
double scanDistance = 2.5 * toolRadiusScaled;
// stepping through path discretized to stepDistance
double stepDistance = min(double(RESOLUTION_FACTOR), interPathLen / 24) + 1;
double stepDistance = min(double(MIN_STEP_CLIPPER), interPathLen / 24) + 1;
const IntPoint* prevPt = &interPath->front();
double distance = 0;
for (size_t j = 1; j < ipc2_size; j++) {
@@ -1742,29 +1755,16 @@ std::list<AdaptiveOutput> Adaptive2d::Execute(
//**********************************
// keep the tolerance in workable range
if (tolerance < 0.01) {
tolerance = 0.01;
}
if (tolerance > 0.2) {
tolerance = 0.2;
}
tolerance = max(tolerance, 0.01);
tolerance = min(tolerance, 1.0);
scaleFactor = RESOLUTION_FACTOR / tolerance;
long maxScaleFactor = toolDiameter < 1.0 ? 10000 : 1000;
if (stepOverFactor * toolDiameter < 1.0) {
scaleFactor *= 1.0 / (stepOverFactor * toolDiameter);
}
if (scaleFactor > maxScaleFactor) {
scaleFactor = maxScaleFactor;
}
// scaleFactor = round(scaleFactor);
// 1/"tolerance" = number of min-size adaptive steps per stepover
scaleFactor = MIN_STEP_CLIPPER / tolerance / min(1.0, stepOverFactor * toolDiameter);
current_region = 0;
cout << "Tool Diameter: " << toolDiameter << endl;
cout << "Accuracy: " << round(10000.0 / scaleFactor) / 10 << " um" << endl;
cout << "Min step size: " << round(MIN_STEP_CLIPPER / scaleFactor * 1000 * 10) / 10 << " um"
<< endl;
cout << flush;
toolRadiusScaled = long(toolDiameter * scaleFactor / 2);
@@ -1912,8 +1912,8 @@ std::list<AdaptiveOutput> Adaptive2d::Execute(
if (opType == OperationType::otProfilingInside || opType == OperationType::otProfilingOutside) {
double offset = opType == OperationType::otProfilingInside
? -2 * (helixRampRadiusScaled + toolRadiusScaled) - RESOLUTION_FACTOR
: 2 * (helixRampRadiusScaled + toolRadiusScaled) + RESOLUTION_FACTOR;
? -2 * (helixRampRadiusScaled + toolRadiusScaled) - MIN_STEP_CLIPPER
: 2 * (helixRampRadiusScaled + toolRadiusScaled) + MIN_STEP_CLIPPER;
for (const auto& current : inputPaths) {
int nesting = getPathNestingLevel(current, inputPaths);
if (nesting % 2 != 0 && (polyTreeNestingLimit == 0 || nesting <= polyTreeNestingLimit)) {
@@ -1990,7 +1990,7 @@ bool Adaptive2d::FindEntryPoint(
for (int iter = 0; iter < 10; iter++) {
clipof.Clear();
clipof.AddPaths(checkPaths, JoinType::jtSquare, EndType::etClosedPolygon);
double step = RESOLUTION_FACTOR;
double step = MIN_STEP_CLIPPER;
double currentDelta = -1;
clipof.Execute(incOffset, currentDelta);
while (!incOffset.empty()) {
@@ -2176,7 +2176,7 @@ bool Adaptive2d::IsAllowedToCutTrough(
else {
Clipper clip;
double distance = sqrt(DistanceSqrd(p1, p2));
double stepSize = min(0.5 * stepOverScaled, 8 * RESOLUTION_FACTOR);
double stepSize = min(0.5 * stepOverScaled, 8 * MIN_STEP_CLIPPER);
if (distance < stepSize / 2) { // not significant cut
Perf_IsAllowedToCutTrough.Stop();
return true;
@@ -2226,7 +2226,7 @@ bool Adaptive2d::ResolveLinkPath(
double directDistance = sqrt(DistanceSqrd(startPoint, endPoint));
Paths linkPaths;
double scanStep = 2 * RESOLUTION_FACTOR;
double scanStep = 2 * MIN_STEP_CLIPPER;
if (scanStep > scaleFactor * 0.1) {
scanStep = scaleFactor * 0.1;
}
@@ -2411,8 +2411,8 @@ bool Adaptive2d::MakeLeadPath(
for (int i = 0; i < 10000; i++) {
if (IsAllowedToCutTrough(
IntPoint(
currentPoint.X + RESOLUTION_FACTOR * nextDir.X,
currentPoint.Y + RESOLUTION_FACTOR * nextDir.Y
currentPoint.X + MIN_STEP_CLIPPER * nextDir.X,
currentPoint.Y + MIN_STEP_CLIPPER * nextDir.Y
),
nextPoint,
clearedArea,
@@ -2478,7 +2478,6 @@ void Adaptive2d::AppendToolPath(
);
IntPoint startPoint(long(lastTPoint.first * scaleFactor), long(lastTPoint.second * scaleFactor));
ClipperOffset clipof;
// first we try to cut through the linking move for short distances
bool linkFound = false;
double linkDistance = sqrt(DistanceSqrd(startPoint, endPoint));
@@ -2834,8 +2833,8 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
EngagePoint engage(engageBounds); // engage point stepping instance
if (outsideEntry) {
engage.moveToClosestPoint(toolPos, 2 * RESOLUTION_FACTOR);
engage.moveForward(RESOLUTION_FACTOR);
engage.moveToClosestPoint(toolPos, 2 * MIN_STEP_CLIPPER);
engage.moveForward(MIN_STEP_CLIPPER);
toolPos = engage.getCurrentPoint();
toolDir = engage.getCurrentDir();
entryPoint = toolPos;
@@ -2849,7 +2848,7 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
output.HelixCenterPoint.first = double(entryPoint.X) / scaleFactor;
output.HelixCenterPoint.second = double(entryPoint.Y) / scaleFactor;
long stepScaled = long(RESOLUTION_FACTOR);
long stepScaled = long(MIN_STEP_CLIPPER);
IntPoint engagePoint;
IntPoint newToolPos;
@@ -2947,24 +2946,24 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
double targetAreaPD = optimalCutAreaPD;
// set the step size
double slowDownDistance = max(double(toolRadiusScaled) / 4, RESOLUTION_FACTOR * 8);
// set the step size: 1x to 8x base size
double slowDownDistance = max(double(toolRadiusScaled) / 4, MIN_STEP_CLIPPER * 8);
if (distanceToBoundary < slowDownDistance || distanceToEngage < slowDownDistance) {
stepScaled = long(RESOLUTION_FACTOR);
stepScaled = long(MIN_STEP_CLIPPER);
}
else if (fabs(angle) > NTOL) {
stepScaled = long(RESOLUTION_FACTOR / fabs(angle));
stepScaled = long(MIN_STEP_CLIPPER / fabs(angle));
}
else {
stepScaled = long(RESOLUTION_FACTOR * 4);
stepScaled = long(MIN_STEP_CLIPPER * 8);
}
// clamp the step size - for stability
if (stepScaled > min(long(toolRadiusScaled / 4), long(RESOLUTION_FACTOR * 8))) {
stepScaled = min(long(toolRadiusScaled / 4), long(RESOLUTION_FACTOR * 8));
if (stepScaled > min(long(toolRadiusScaled / 4), long(MIN_STEP_CLIPPER * 8))) {
stepScaled = min(long(toolRadiusScaled / 4), long(MIN_STEP_CLIPPER * 8));
}
if (stepScaled < RESOLUTION_FACTOR) {
stepScaled = long(RESOLUTION_FACTOR);
if (stepScaled < MIN_STEP_CLIPPER) {
stepScaled = long(MIN_STEP_CLIPPER);
}
//*****************************
@@ -2979,22 +2978,30 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
Perf_PointIterations.Start();
int iteration;
double prev_error = __DBL_MAX__;
bool pointNotInterp;
for (iteration = 0; iteration < MAX_ITERATIONS; iteration++) {
total_iterations++;
if (iteration == 0) {
angle = predictedAngle;
pointNotInterp = true;
}
else if (iteration == 1) {
angle = interp.MIN_ANGLE; // max engage
pointNotInterp = true;
}
else if (iteration == 3) {
angle = interp.MAX_ANGLE; // min engage
}
else if (interp.getPointCount() < 2) {
angle = interp.getRandomAngle();
else if (iteration == 2) {
if (interp.bothSides()) {
angle = interp.interpolateAngle();
pointNotInterp = false;
}
else {
angle = interp.MAX_ANGLE; // min engage
pointNotInterp = true;
}
}
else {
angle = interp.interpolateAngle(targetAreaPD);
angle = interp.interpolateAngle();
pointNotInterp = false;
}
angle = interp.clampAngle(angle);
@@ -3004,11 +3011,51 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
long(toolPos.Y + newToolDir.Y * stepScaled)
);
// Skip iteration if this IntPoint has already been processed
bool intRepeat = false;
if (interp.m_min && newToolPos == interp.m_min->first.second) {
interp.m_min = {{angle, newToolPos}, interp.m_min->second};
intRepeat = true;
}
if (interp.m_max && newToolPos == interp.m_max->first.second) {
interp.m_max = {{angle, newToolPos}, interp.m_max->second};
intRepeat = true;
}
if (intRepeat) {
if (interp.m_min && interp.m_max
&& abs(interp.m_min->first.second.X - interp.m_max->first.second.X) <= 1
&& abs(interp.m_min->first.second.Y - interp.m_max->first.second.Y) <= 1) {
if (pointNotInterp) {
// if this happens while testing min/max of the range it doesn't mean
// anything; only exit early if interpolation is down to adjacent
// integers
continue;
}
// exit early, selecting the better of the two adjacent integers
double error;
if (abs(interp.m_min->second) < abs(interp.m_max->second)) {
newToolDir = rotate(toolDir, interp.m_min->first.first);
newToolPos = interp.m_min->first.second;
error = interp.m_min->second;
}
else {
newToolDir = rotate(toolDir, interp.m_max->first.first);
newToolPos = interp.m_max->first.second;
error = interp.m_max->second;
}
areaPD = error + targetAreaPD;
area = areaPD * double(stepScaled);
break;
}
continue;
}
area = CalcCutArea(clip, toolPos, newToolPos, cleared);
areaPD = area / double(stepScaled); // area per distance
interp.addPoint(areaPD, angle);
double error = areaPD - targetAreaPD;
interp.addPoint(error, {angle, newToolPos}, pointNotInterp);
// cout << " iter:" << iteration << " angle:" << angle << " area:" << areaPD
// << " target:" << targetAreaPD << " error:" << error << " max:" << maxError
// << endl;
@@ -3019,9 +3066,6 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
}
break;
}
if (iteration > 5 && fabs(error - prev_error) < 0.001) {
break;
}
if (iteration == MAX_ITERATIONS - 1) {
total_exceeded++;
}
@@ -3091,8 +3135,7 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
prevDistTrend = distanceTrend;
prevDistFromStart = distFromStart;
if (area > 0.5 * MIN_CUT_AREA_FACTOR * optimalCutAreaPD
* RESOLUTION_FACTOR) { // cut is ok - record it
if (area > 0.5 * MIN_CUT_AREA_FACTOR * optimalCutAreaPD) { // cut is ok - record it
noCutDistance = 0;
if (toClearPath.empty()) {
toClearPath.push_back(toolPos);
@@ -3155,7 +3198,7 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
cleared.ExpandCleared(toClearPath);
toClearPath.clear();
}
if (cumulativeCutArea > MIN_CUT_AREA_FACTOR * optimalCutAreaPD * RESOLUTION_FACTOR) {
if (cumulativeCutArea > MIN_CUT_AREA_FACTOR * optimalCutAreaPD) {
Path cleaned;
CleanPath(passToolPath, cleaned, CLEAN_PATH_TOLERANCE);
total_output_points += long(cleaned.size());
@@ -3185,7 +3228,7 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
this,
cleared,
moveDistance,
ENGAGE_AREA_THR_FACTOR * optimalCutAreaPD * RESOLUTION_FACTOR,
ENGAGE_AREA_THR_FACTOR * optimalCutAreaPD,
4 * referenceCutArea * stepOverFactor
)) {
// check if there are any uncleared area left
@@ -3223,7 +3266,7 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
this,
cleared,
moveDistance,
ENGAGE_AREA_THR_FACTOR * optimalCutAreaPD * RESOLUTION_FACTOR,
ENGAGE_AREA_THR_FACTOR * optimalCutAreaPD,
4 * referenceCutArea * stepOverFactor
)) {
break;

View File

@@ -200,7 +200,7 @@ private:
void ApplyStockToLeave(Paths& inputPaths);
private: // constants for fine tuning
const double RESOLUTION_FACTOR = 16.0;
const double MIN_STEP_CLIPPER = 16.0;
const int MAX_ITERATIONS = 10;
const double AREA_ERROR_FACTOR = 0.05; /* how precise to match the cut area to optimal,
reasonable value: 0.05 = 5%*/
@@ -208,9 +208,9 @@ private: // constants for fine tuning
const int DIRECTION_SMOOTHING_BUFLEN = 3; // gyro points - used for angle smoothing
const double MIN_CUT_AREA_FACTOR = 0.1; // used for filtering out of insignificant cuts (should
// be < ENGAGE_AREA_THR_FACTOR)
const double ENGAGE_AREA_THR_FACTOR = 0.5; // influences minimal engage area
const double MIN_CUT_AREA_FACTOR = 0.1
* 16; // used for filtering out of insignificant cuts (should be < ENGAGE_AREA_THR_FACTOR)
const double ENGAGE_AREA_THR_FACTOR = 0.5 * 16; // influences minimal engage area
const double ENGAGE_SCAN_DISTANCE_FACTOR = 0.2; // influences the engage scan/stepping distance
const double CLEAN_PATH_TOLERANCE = 1.41; // should be >1