[CAM] initial implementation of automatic helix size selection for adaptive

This commit is contained in:
David Kaufman
2025-08-29 11:15:28 -04:00
parent 65ed897c2b
commit b203ba4ec5
4 changed files with 110 additions and 34 deletions

View File

@@ -118,7 +118,7 @@ def discretize(edge, flipDirection=False):
return pts
def GenerateGCode(op, obj, adaptiveResults, helixDiameter):
def GenerateGCode(op, obj, adaptiveResults):
if not adaptiveResults or not adaptiveResults[0]["AdaptivePaths"]:
return
@@ -699,7 +699,8 @@ def ExecuteModelAware(op, obj):
FreeCADGui.updateGui()
try:
helixDiameter = obj.HelixDiameterLimit.Value
helixDiameter = obj.HelixIdealDiameterPercent / 100 * op.tool.Diameter.Value
helixMinDiameter = obj.HelixMinDiameterPercent / 100 * op.tool.Diameter.Value
topZ = op.stock.Shape.BoundBox.ZMax
obj.Stopped = False
obj.StopProcessing = False
@@ -774,6 +775,7 @@ def ExecuteModelAware(op, obj):
"stockGeometry": stockPaths,
"stepover": obj.StepOver,
"effectiveHelixDiameter": helixDiameter,
"helixMinDiameter": helixMinDiameter,
"operationType": "Clearing",
"side": "Outside",
"forceInsideOut": obj.ForceInsideOut,
@@ -793,6 +795,7 @@ def ExecuteModelAware(op, obj):
"stockGeometry": stockPaths,
"stepover": obj.StepOver,
"effectiveHelixDiameter": helixDiameter,
"helixMinDiameter": helixMinDiameter,
"operationType": "Clearing",
"side": "Inside",
"forceInsideOut": obj.ForceInsideOut,
@@ -857,7 +860,8 @@ def ExecuteModelAware(op, obj):
a2d = area.Adaptive2d()
a2d.stepOverFactor = 0.01 * obj.StepOver
a2d.toolDiameter = op.tool.Diameter.Value
a2d.helixRampDiameter = helixDiameter
a2d.helixRampTargetDiameter = helixDiameter
a2d.helixRampMinDiameter = helixMinDiameter
a2d.keepToolDownDistRatio = keepToolDownRatio
# NOTE: Z stock is handled in our stepdowns
a2d.stockToLeave = obj.StockToLeave.Value
@@ -946,7 +950,7 @@ def ExecuteModelAware(op, obj):
)
# GENERATE
GenerateGCode(op, obj, adaptiveResults, helixDiameter)
GenerateGCode(op, obj, adaptiveResults)
if not obj.StopProcessing:
Path.Log.info("*** Done. Elapsed time: %f sec\n\n" % (time.time() - start))
@@ -1810,12 +1814,21 @@ class PathAdaptive(PathOp.ObjectOp):
),
)
obj.addProperty(
"App::PropertyLength",
"HelixDiameterLimit",
"App::PropertyPercent",
"HelixIdealDiameterPercent",
"Adaptive",
QT_TRANSLATE_NOOP(
"App::Property",
"Limit helix entry diameter, if limit larger than tool diameter or 0, tool diameter is used",
"Ideal helix entry diameter, as a percentage of the tool diameter",
),
)
obj.addProperty(
"App::PropertyPercent",
"HelixMinDiameterPercent",
"Adaptive",
QT_TRANSLATE_NOOP(
"App::Property",
"Minimum acceptable helix entry diameter, as a percentage of the tool diameter",
),
)
obj.addProperty(
@@ -1874,7 +1887,8 @@ class PathAdaptive(PathOp.ObjectOp):
obj.StopProcessing = False
obj.HelixAngle = 5
obj.HelixConeAngle = 0
obj.HelixDiameterLimit = 0.0
obj.HelixIdealDiameterPercent = 100
obj.HelixMinDiameterPercent = 10
obj.AdaptiveInputState = ""
obj.AdaptiveOutputState = ""
obj.StockToLeave = 0
@@ -1964,6 +1978,33 @@ class PathAdaptive(PathOp.ObjectOp):
obj.addProperty("Part::PropertyPartShape", "removalshape", "Path", "")
obj.setEditorMode("removalshape", 2) # hide
if hasattr(obj, "HelixDiameterLimit"):
oldD = obj.HelixDiameterLimit
obj.removeProperty("HelixDiameterLimit")
obj.addProperty(
"App::PropertyPercent",
"HelixIdealDiameterPercent",
"Adaptive",
QT_TRANSLATE_NOOP(
"App::Property",
"Ideal helix entry diameter, as a percentage of the tool diameter",
),
)
obj.addProperty(
"App::PropertyPercent",
"HelixMinDiameterPercent",
"Adaptive",
QT_TRANSLATE_NOOP(
"App::Property",
"Minimum acceptable helix entry diameter, as a percentage of the tool diameter",
),
)
obj.HelixMinDiameterPercent = 10
if hasattr(obj, "ToolController"):
obj.HelixIdealDiameterPercent = (
75 if oldD == 0 else 100 * oldD / obj.ToolController.Tool.Diameter.Value
)
FeatureExtensions.initialize_properties(obj)
@@ -1986,7 +2027,8 @@ def SetupProperties():
"AdaptiveOutputState",
"HelixAngle",
"HelixConeAngle",
"HelixDiameterLimit",
"HelixIdealDiameterPercent",
"HelixMinDiameterPercent",
"UseOutline",
"OrderCutsByRegion",
]

View File

@@ -1773,17 +1773,15 @@ std::list<AdaptiveOutput> Adaptive2d::Execute(
lastProgressTime = clock();
stopProcessing = false;
if (helixRampDiameter < NTOL) {
helixRampDiameter = 0.75 * toolDiameter;
}
if (helixRampDiameter > toolDiameter) {
helixRampDiameter = toolDiameter;
}
if (helixRampDiameter < toolDiameter / 8) {
helixRampDiameter = toolDiameter / 8;
if (helixRampTargetDiameter < NTOL) {
helixRampTargetDiameter = toolDiameter;
}
helixRampTargetDiameter = min(helixRampTargetDiameter, toolDiameter);
helixRampMinDiameter = max(helixRampMinDiameter, toolDiameter / 8);
helixRampTargetDiameter = max(helixRampTargetDiameter, helixRampMinDiameter);
helixRampRadiusScaled = long(helixRampDiameter * scaleFactor / 2);
helixRampMaxRadiusScaled = long(helixRampTargetDiameter * scaleFactor / 2);
helixRampMinRadiusScaled = long(helixRampMinDiameter * scaleFactor / 2);
if (finishingProfile) {
finishPassOffsetScaled = long(stepOverScaled / 10);
}
@@ -1812,7 +1810,7 @@ std::list<AdaptiveOutput> Adaptive2d::Execute(
#ifdef DEV_MODE
cout << "optimalCutAreaPD:" << optimalCutAreaPD << " scaleFactor:" << scaleFactor
<< " toolRadiusScaled:" << toolRadiusScaled
<< " helixRampRadiusScaled:" << helixRampRadiusScaled << endl;
<< " helixRampMaxRadiusScaled:" << helixRampMaxRadiusScaled << endl;
#endif
//******************************
// Convert input paths to clipper
@@ -1912,8 +1910,8 @@ std::list<AdaptiveOutput> Adaptive2d::Execute(
if (opType == OperationType::otProfilingInside || opType == OperationType::otProfilingOutside) {
double offset = opType == OperationType::otProfilingInside
? -2 * (helixRampRadiusScaled + toolRadiusScaled) - MIN_STEP_CLIPPER
: 2 * (helixRampRadiusScaled + toolRadiusScaled) + MIN_STEP_CLIPPER;
? -2 * (helixRampMaxRadiusScaled + toolRadiusScaled) - MIN_STEP_CLIPPER
: 2 * (helixRampMaxRadiusScaled + toolRadiusScaled) + MIN_STEP_CLIPPER;
for (const auto& current : inputPaths) {
int nesting = getPathNestingLevel(current, inputPaths);
if (nesting % 2 != 0 && (polyTreeNestingLimit == 0 || nesting <= polyTreeNestingLimit)) {
@@ -1977,7 +1975,8 @@ bool Adaptive2d::FindEntryPoint(
ClearedArea& clearedArea /*output-initial cleared area by helix*/,
IntPoint& entryPoint /*output*/,
IntPoint& toolPos,
DoublePoint& toolDir
DoublePoint& toolDir,
long& helixRadiusScaled
)
{
Paths incOffset;
@@ -2018,13 +2017,12 @@ bool Adaptive2d::FindEntryPoint(
}
}
// check if helix fits
if (found) {
// make initial polygon cleared by helix ramp
const auto checkHelixFit = [&](long testHelixRadiusScaled) {
clipof.Clear();
Path p1;
p1.push_back(entryPoint);
clipof.AddPath(p1, JoinType::jtRound, EndType::etOpenRound);
clipof.Execute(clearedPaths, helixRampRadiusScaled + toolRadiusScaled);
clipof.Execute(clearedPaths, (double)(testHelixRadiusScaled + toolRadiusScaled));
CleanPolygons(clearedPaths);
// we got first cleared area - check if it is crossing boundary
clip.Clear();
@@ -2032,11 +2030,32 @@ bool Adaptive2d::FindEntryPoint(
clip.AddPaths(boundPaths, PolyType::ptClip, true);
Paths crossing;
clip.Execute(ClipType::ctDifference, crossing);
if (!crossing.empty()) {
// helix does not fit to the cutting area
return crossing.empty();
};
if (found) {
// check that helix fits, and make initial polygon cleared by helix ramp
if (!checkHelixFit(helixRampMinRadiusScaled)) {
// min-size helix does not fit
found = false;
}
else {
// find the largest helix that fits
// minSize = largest known fit; maxSize = largest possible fit
long minSize = helixRampMinRadiusScaled;
long maxSize = helixRampMaxRadiusScaled;
while (minSize < maxSize) {
long testSize = (minSize + maxSize + 1) / 2; // always testSize > minSize
if (checkHelixFit(testSize)) {
minSize = testSize;
}
else {
maxSize = testSize - 1; // always maxSize >= minSize
}
}
helixRadiusScaled = minSize;
checkHelixFit(helixRadiusScaled); // set clearedPaths for final size
clearedArea.SetClearedPaths(clearedPaths);
}
}
@@ -2070,14 +2089,15 @@ bool Adaptive2d::FindEntryPoint(
hp << entryPoint;
clipof.AddPath(hp, JoinType::jtRound, EndType::etOpenRound);
Paths hps;
clipof.Execute(hps, helixRampRadiusScaled);
clipof.Execute(hps, helixRadiusScaled);
AddPathsToProgress(progressPaths, hps);
toolPos = IntPoint(entryPoint.X, entryPoint.Y - helixRampRadiusScaled);
toolPos = IntPoint(entryPoint.X, entryPoint.Y - helixRadiusScaled);
toolDir = DoublePoint(1.0, 0.0);
}
return found;
}
bool Adaptive2d::FindEntryPointOutside(
TPaths& progressPaths,
const Paths& toolBoundPaths,
@@ -2780,6 +2800,7 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
ClipperOffset clipof;
IntPoint entryPoint;
long helixRadiusScaled;
TPaths progressPaths;
progressPaths.reserve(10000);
@@ -2824,7 +2845,16 @@ void Adaptive2d::ProcessPolyNode(Paths boundPaths, Paths toolBoundPaths)
outsideEntry = true;
}
else {
if (!FindEntryPoint(progressPaths, toolBoundPaths, boundPaths, cleared, entryPoint, toolPos, toolDir)) {
if (!FindEntryPoint(
progressPaths,
toolBoundPaths,
boundPaths,
cleared,
entryPoint,
toolPos,
toolDir,
helixRadiusScaled
)) {
Perf_ProcessPolyNode.Stop();
return;
}

View File

@@ -87,7 +87,8 @@ class Adaptive2d
public:
Adaptive2d();
double toolDiameter = 5;
double helixRampDiameter = 0;
double helixRampTargetDiameter = 0;
double helixRampMinDiameter = 0;
double stepOverFactor = 0.2;
double tolerance = 0.1;
double stockToLeave = 0;
@@ -118,7 +119,8 @@ private:
double stepOverScaled = 1;
long toolRadiusScaled = 10;
long finishPassOffsetScaled = 0;
long helixRampRadiusScaled = 0;
long helixRampMaxRadiusScaled = 0;
long helixRampMinRadiusScaled = 0;
double referenceCutArea = 0;
double optimalCutAreaPD = 0;
bool stopProcessing = false;
@@ -136,7 +138,8 @@ private:
ClearedArea& cleared /*output*/,
IntPoint& entryPoint /*output*/,
IntPoint& toolPos,
DoublePoint& toolDir
DoublePoint& toolDir,
long& helixRadiusScaled
);
bool FindEntryPointOutside(
TPaths& progressPaths,

View File

@@ -426,7 +426,8 @@ void init_pyarea(py::module& m)
.def_readwrite("stepOverFactor", &Adaptive2d::stepOverFactor)
.def_readwrite("toolDiameter", &Adaptive2d::toolDiameter)
.def_readwrite("stockToLeave", &Adaptive2d::stockToLeave)
.def_readwrite("helixRampDiameter", &Adaptive2d::helixRampDiameter)
.def_readwrite("helixRampTargetDiameter", &Adaptive2d::helixRampTargetDiameter)
.def_readwrite("helixRampMinDiameter", &Adaptive2d::helixRampMinDiameter)
.def_readwrite("forceInsideOut", &Adaptive2d::forceInsideOut)
.def_readwrite("finishingProfile", &Adaptive2d::finishingProfile)
//.def_readwrite("polyTreeNestingLimit", &Adaptive2d::polyTreeNestingLimit)