Merge pull request #9782 from davidgilkaufman/restMachining
Rest Machining for Path and Path3D Operations
This commit is contained in:
@@ -496,6 +496,79 @@ void Area::add(const TopoDS_Shape& shape, short op) {
|
||||
myShapes.emplace_back(op, shape);
|
||||
}
|
||||
|
||||
std::shared_ptr<Area> Area::getClearedArea(double tipDiameter, double diameter) {
|
||||
build();
|
||||
#define AREA_MY(_param) myParams.PARAM_FNAME(_param)
|
||||
PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_OFFSET_CONF);
|
||||
PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL);
|
||||
|
||||
// Do not fit arcs after these offsets; it introduces unnecessary approximation error, and all off
|
||||
// those arcs will be converted back to segments again for clipper differencing in getRestArea anyway
|
||||
CAreaConfig conf(myParams, /*no_fit_arcs*/ true);
|
||||
|
||||
const double roundPrecision = myParams.Accuracy;
|
||||
const double buffer = 2 * roundPrecision;
|
||||
|
||||
// A = myArea
|
||||
// prevCenters = offset(A, -rTip)
|
||||
const double rTip = tipDiameter / 2.;
|
||||
CArea prevCenter(*myArea);
|
||||
prevCenter.OffsetWithClipper(-rTip, JoinType, EndType, myParams.MiterLimit, roundPrecision);
|
||||
|
||||
// prevCleared = offset(prevCenter, r).
|
||||
CArea prevCleared(prevCenter);
|
||||
prevCleared.OffsetWithClipper(diameter / 2. + buffer, JoinType, EndType, myParams.MiterLimit, roundPrecision);
|
||||
|
||||
std::shared_ptr<Area> clearedArea = make_shared<Area>(*this);
|
||||
clearedArea->myArea.reset(new CArea(prevCleared));
|
||||
|
||||
return clearedArea;
|
||||
}
|
||||
|
||||
std::shared_ptr<Area> Area::getRestArea(std::vector<std::shared_ptr<Area>> clearedAreas, double diameter) {
|
||||
build();
|
||||
PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_OFFSET_CONF);
|
||||
PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL);
|
||||
|
||||
const double roundPrecision = myParams.Accuracy;
|
||||
const double buffer = 2 * roundPrecision;
|
||||
|
||||
// transform all clearedAreas into our workplane
|
||||
Area clearedAreasInPlane(&myParams);
|
||||
clearedAreasInPlane.myArea.reset(new CArea());
|
||||
for (std::shared_ptr<Area> clearedArea : clearedAreas) {
|
||||
gp_Trsf trsf = clearedArea->myTrsf;
|
||||
trsf.Invert();
|
||||
trsf.SetTranslationPart(gp_Vec{trsf.TranslationPart().X(), trsf.TranslationPart().Y(), -myTrsf.TranslationPart().Z()}); // discard z-height of cleared workplane, set to myWorkPlane's height
|
||||
TopoDS_Shape clearedShape = Area::toShape(*clearedArea->myArea, false, &trsf);
|
||||
Area::addShape(*(clearedAreasInPlane.myArea), clearedShape, &myTrsf, .01 /*default value*/,
|
||||
&myWorkPlane);
|
||||
}
|
||||
|
||||
// remaining = A - prevCleared
|
||||
CArea remaining(*myArea);
|
||||
remaining.Clip(toClipperOp(Area::OperationDifference), &*(clearedAreasInPlane.myArea), SubjectFill, ClipFill);
|
||||
|
||||
// rest = intersect(A, offset(remaining, dTool))
|
||||
CArea restCArea(remaining);
|
||||
restCArea.OffsetWithClipper(diameter + buffer, JoinType, EndType, myParams.MiterLimit, roundPrecision);
|
||||
restCArea.Clip(toClipperOp(Area::OperationIntersection), &*myArea, SubjectFill, ClipFill);
|
||||
|
||||
gp_Trsf trsf(myTrsf.Inverted());
|
||||
TopoDS_Shape restShape = Area::toShape(restCArea, false, &trsf);
|
||||
std::shared_ptr<Area> restArea = make_shared<Area>(&myParams);
|
||||
restArea->add(restShape, OperationCompound);
|
||||
|
||||
return restArea;
|
||||
}
|
||||
|
||||
TopoDS_Shape Area::toTopoShape()
|
||||
{
|
||||
build();
|
||||
gp_Trsf trsf = myTrsf.Inverted();
|
||||
return toShape(*myArea, false, &trsf);
|
||||
}
|
||||
|
||||
|
||||
void Area::setParams(const AreaParams& params) {
|
||||
#define AREA_SRC(_param) params.PARAM_FNAME(_param)
|
||||
@@ -1624,7 +1697,6 @@ void Area::build() {
|
||||
if (myShapes.empty())
|
||||
throw Base::ValueError("no shape added");
|
||||
|
||||
#define AREA_MY(_param) myParams.PARAM_FNAME(_param)
|
||||
PARAM_ENUM_CONVERT(AREA_MY, PARAM_FNAME, PARAM_ENUM_EXCEPT, AREA_PARAMS_CLIPPER_FILL);
|
||||
|
||||
if (myHaveSolid && myParams.SectionCount) {
|
||||
|
||||
@@ -242,6 +242,10 @@ public:
|
||||
const std::vector<double>& heights = std::vector<double>(),
|
||||
const TopoDS_Shape& plane = TopoDS_Shape());
|
||||
|
||||
std::shared_ptr<Area> getClearedArea(double tipDiameter, double diameter);
|
||||
std::shared_ptr<Area> getRestArea(std::vector<std::shared_ptr<Area>> clearedAreas, double diameter);
|
||||
TopoDS_Shape toTopoShape();
|
||||
|
||||
/** Config this Area object */
|
||||
void setParams(const AreaParams& params);
|
||||
|
||||
|
||||
@@ -62,6 +62,21 @@ same algorithm</UserDocu>
|
||||
<UserDocu></UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="getClearedArea" Keyword="true">
|
||||
<Documentation>
|
||||
<UserDocu></UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="getRestArea" Keyword="true">
|
||||
<Documentation>
|
||||
<UserDocu></UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="toTopoShape" Keyword="true">
|
||||
<Documentation>
|
||||
<UserDocu></UserDocu>
|
||||
</Documentation>
|
||||
</Methode>
|
||||
<Methode Name="setParams" Keyword="true">
|
||||
<Documentation>
|
||||
<UserDocu></UserDocu>
|
||||
|
||||
@@ -145,6 +145,22 @@ static const PyMethodDef areaOverrides[] = {
|
||||
"\n* plane (None): optional shape to specify a section plane. If not give, the current workplane\n"
|
||||
"of this Area is used if section mode is 'Workplane'.",
|
||||
},
|
||||
{
|
||||
"getClearedArea",nullptr,0,
|
||||
"getClearedArea(tipDiameter, diameter):\n"
|
||||
"Gets the area cleared when a tool maximally clears this area. This method assumes a tool tip diameter 'tipDiameter' traces the full area, and that (perhaps at a different height on the tool) this clears a different region with tool diameter 'diameter'.\n",
|
||||
},
|
||||
{
|
||||
"getRestArea",nullptr,0,
|
||||
"getRestArea(clearedAreas, diameter):\n"
|
||||
"Rest machining: gets the area left to be machined, assuming some of this area has already been cleared previous tool paths.\n"
|
||||
"clearedAreas: the regions already cleared.\n"
|
||||
"diameter: the tool diameter that finishes clearing this area.\n",
|
||||
},
|
||||
{
|
||||
"toTopoShape",nullptr,0,
|
||||
"toTopoShape():\n"
|
||||
},
|
||||
{
|
||||
"setDefaultParams",reinterpret_cast<PyCFunction>(reinterpret_cast<void (*) ()>(areaSetParams)), METH_VARARGS|METH_KEYWORDS|METH_STATIC,
|
||||
"setDefaultParams(key=value...):\n"
|
||||
@@ -383,6 +399,55 @@ PyObject* AreaPy::makeSections(PyObject *args, PyObject *keywds)
|
||||
} PY_CATCH_OCC
|
||||
}
|
||||
|
||||
PyObject* AreaPy::getClearedArea(PyObject *args, PyObject *keywds)
|
||||
{
|
||||
PY_TRY {
|
||||
double tipDiameter, diameter;
|
||||
if (!PyArg_ParseTuple(args, "dd", &tipDiameter, &diameter))
|
||||
return nullptr;
|
||||
std::shared_ptr<Area> clearedArea = getAreaPtr()->getClearedArea(tipDiameter, diameter);
|
||||
auto pyClearedArea = Py::asObject(new AreaPy(new Area(*clearedArea, true)));
|
||||
return Py::new_reference_to(pyClearedArea);
|
||||
} PY_CATCH_OCC
|
||||
}
|
||||
|
||||
PyObject* AreaPy::getRestArea(PyObject *args, PyObject *keywds)
|
||||
{
|
||||
PY_TRY {
|
||||
PyObject *pyClearedAreas;
|
||||
std::vector<std::shared_ptr<Area>> clearedAreas;
|
||||
double diameter;
|
||||
if (!PyArg_ParseTuple(args, "Od", &pyClearedAreas, &diameter))
|
||||
return nullptr;
|
||||
if (pyClearedAreas && PyObject_TypeCheck(pyClearedAreas, &PyList_Type)) {
|
||||
Py::Sequence clearedAreasSeq(pyClearedAreas);
|
||||
clearedAreas.reserve(clearedAreasSeq.size());
|
||||
for (Py::Sequence::iterator it = clearedAreasSeq.begin(); it != clearedAreasSeq.end(); ++it) {
|
||||
PyObject *item = (*it).ptr();
|
||||
if (!PyObject_TypeCheck(item, &(AreaPy::Type))) {
|
||||
PyErr_SetString(PyExc_TypeError, "cleared areas must only contain AreaPy type");
|
||||
return nullptr;
|
||||
}
|
||||
clearedAreas.push_back(std::make_shared<Area>(*static_cast<AreaPy*>(item)->getAreaPtr(), true));
|
||||
}
|
||||
} else {
|
||||
PyErr_SetString(PyExc_TypeError, "clearedAreas must be of type list of AreaPy");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<Area> restArea = getAreaPtr()->getRestArea(clearedAreas, diameter);
|
||||
auto pyRestArea = Py::asObject(new AreaPy(new Area(*restArea, true)));
|
||||
return Py::new_reference_to(pyRestArea);
|
||||
} PY_CATCH_OCC
|
||||
}
|
||||
|
||||
PyObject* AreaPy::toTopoShape(PyObject *args, PyObject *keywds)
|
||||
{
|
||||
PY_TRY {
|
||||
return Py::new_reference_to(Part::shape2pyshape(getAreaPtr()->toTopoShape()));
|
||||
} PY_CATCH_OCC
|
||||
}
|
||||
|
||||
PyObject* AreaPy::setDefaultParams(PyObject *, PyObject *)
|
||||
{
|
||||
return nullptr;
|
||||
|
||||
@@ -245,14 +245,24 @@ The latter can be used to face of the entire stock area to ensure uniform height
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="2" column="1">
|
||||
<widget class="QCheckBox" name="minTravel">
|
||||
<property name="text">
|
||||
<string>Min Travel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
<item row="3" column="0">
|
||||
<widget class="QCheckBox" name="useRestMachining">
|
||||
<property name="toolTip">
|
||||
<string>Check to skip machining regions that have already been cleared by previous operations</string>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Use Rest Machining</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
|
||||
@@ -38,7 +38,7 @@ __title__ = "Base class for PathArea based operations."
|
||||
__author__ = "sliptonic (Brad Collette)"
|
||||
__url__ = "https://www.freecad.org"
|
||||
__doc__ = "Base class and properties for Path.Area based operations."
|
||||
__contributors__ = "russ4262 (Russell Johnson)"
|
||||
__contributors__ = "russ4262 (Russell Johnson) davidgilkaufman (David Kaufman)"
|
||||
|
||||
|
||||
if False:
|
||||
@@ -236,6 +236,47 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
mode=0, project=self.areaOpUseProjection(obj), heights=heights
|
||||
)
|
||||
Path.Log.debug("sections = %s" % sections)
|
||||
|
||||
# Rest machining
|
||||
self.sectionShapes = self.sectionShapes + [section.toTopoShape() for section in sections]
|
||||
if hasattr(obj, "UseRestMachining") and obj.UseRestMachining:
|
||||
# Loop through prior operations
|
||||
clearedAreas = []
|
||||
foundSelf = False
|
||||
for op in self.job.Operations.Group:
|
||||
if foundSelf:
|
||||
break
|
||||
oplist = [op] + op.OutListRecursive
|
||||
oplist = list(filter(lambda op: hasattr(op, "Active"), oplist))
|
||||
for op in oplist:
|
||||
if op.Proxy == self:
|
||||
# Ignore self, and all later operations
|
||||
foundSelf = True
|
||||
break
|
||||
if hasattr(op, "RestMachiningRegions") and op.Active:
|
||||
if hasattr(op, "RestMachiningRegionsNeedRecompute") and op.RestMachiningRegionsNeedRecompute:
|
||||
Path.Log.warning(
|
||||
translate("PathAreaOp", "Previous operation %s is required for rest machining, but it has no stored rest machining metadata. Recomputing to generate this metadata...") % op.Label
|
||||
)
|
||||
op.recompute()
|
||||
|
||||
tool = op.Proxy.tool if hasattr(op.Proxy, "tool") else op.ToolController.Proxy.getTool(op.ToolController)
|
||||
diameter = tool.Diameter.getValueAs("mm")
|
||||
def shapeToArea(shape):
|
||||
area = Path.Area()
|
||||
area.setPlane(PathUtils.makeWorkplane(shape))
|
||||
area.add(shape)
|
||||
return area
|
||||
opClearedAreas = [shapeToArea(pa).getClearedArea(diameter, diameter) for pa in op.RestMachiningRegions.SubShapes]
|
||||
clearedAreas.extend(opClearedAreas)
|
||||
restSections = []
|
||||
for section in sections:
|
||||
z = section.getShape().BoundBox.ZMin
|
||||
sectionClearedAreas = [a for a in clearedAreas if a.getShape().BoundBox.ZMax <= z]
|
||||
restSection = section.getRestArea(sectionClearedAreas, self.tool.Diameter.getValueAs("mm"))
|
||||
restSections.append(restSection)
|
||||
sections = restSections
|
||||
|
||||
shapelist = [sec.getShape() for sec in sections]
|
||||
Path.Log.debug("shapelist = %s" % shapelist)
|
||||
|
||||
@@ -388,6 +429,7 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
shapes = [j["shape"] for j in locations]
|
||||
|
||||
sims = []
|
||||
self.sectionShapes = []
|
||||
for shape, isHole, sub in shapes:
|
||||
profileEdgesIsOpen = False
|
||||
|
||||
@@ -435,6 +477,10 @@ class ObjectOp(PathOp.ObjectOp):
|
||||
)
|
||||
)
|
||||
|
||||
if hasattr(obj, "RestMachiningRegions"):
|
||||
obj.RestMachiningRegions = Part.makeCompound(self.sectionShapes)
|
||||
if hasattr(obj, "RestMachiningRegionsNeedRecompute"):
|
||||
obj.RestMachiningRegionsNeedRecompute = False
|
||||
Path.Log.debug("obj.Name: " + str(obj.Name) + "\n\n")
|
||||
return sims
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
|
||||
|
||||
def pocketFeatures(self):
|
||||
"""pocketFeatures() ... return FeaturePocket (see PathPocketBaseGui)"""
|
||||
return PathPocketBaseGui.FeaturePocket
|
||||
return PathPocketBaseGui.FeaturePocket | PathPocketBaseGui.FeatureRestMachining
|
||||
|
||||
|
||||
Command = PathOpGui.SetupOperation(
|
||||
|
||||
@@ -44,6 +44,7 @@ translate = FreeCAD.Qt.translate
|
||||
FeaturePocket = 0x01
|
||||
FeatureFacing = 0x02
|
||||
FeatureOutline = 0x04
|
||||
FeatureRestMachining = 0x08
|
||||
|
||||
|
||||
class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
@@ -89,6 +90,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
if not (FeatureOutline & self.pocketFeatures()):
|
||||
form.useOutline.hide()
|
||||
|
||||
if not (FeatureRestMachining & self.pocketFeatures()):
|
||||
form.useRestMachining.hide()
|
||||
|
||||
# if True:
|
||||
# # currently doesn't have an effect or is experimental
|
||||
# form.minTravel.hide()
|
||||
@@ -131,6 +135,9 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
if obj.UseStartPoint != self.form.useStartPoint.isChecked():
|
||||
obj.UseStartPoint = self.form.useStartPoint.isChecked()
|
||||
|
||||
if obj.UseRestMachining != self.form.useRestMachining.isChecked():
|
||||
obj.UseRestMachining = self.form.useRestMachining.isChecked()
|
||||
|
||||
if FeatureOutline & self.pocketFeatures():
|
||||
if obj.UseOutline != self.form.useOutline.isChecked():
|
||||
obj.UseOutline = self.form.useOutline.isChecked()
|
||||
@@ -155,6 +162,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
).UserString
|
||||
)
|
||||
self.form.useStartPoint.setChecked(obj.UseStartPoint)
|
||||
self.form.useRestMachining.setChecked(obj.UseRestMachining)
|
||||
if FeatureOutline & self.pocketFeatures():
|
||||
self.form.useOutline.setChecked(obj.UseOutline)
|
||||
|
||||
@@ -186,6 +194,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage):
|
||||
signals.append(self.form.toolController.currentIndexChanged)
|
||||
signals.append(self.form.extraOffset.editingFinished)
|
||||
signals.append(self.form.useStartPoint.clicked)
|
||||
signals.append(self.form.useRestMachining.clicked)
|
||||
signals.append(self.form.useOutline.clicked)
|
||||
signals.append(self.form.minTravel.clicked)
|
||||
signals.append(self.form.coolantController.currentIndexChanged)
|
||||
|
||||
@@ -52,7 +52,7 @@ class TaskPanelOpPage(PathPocketBaseGui.TaskPanelOpPage):
|
||||
|
||||
def pocketFeatures(self):
|
||||
"""pocketFeatures() ... return FeaturePocket (see PathPocketBaseGui)"""
|
||||
return PathPocketBaseGui.FeaturePocket | PathPocketBaseGui.FeatureOutline
|
||||
return PathPocketBaseGui.FeaturePocket | PathPocketBaseGui.FeatureOutline | PathPocketBaseGui.FeatureRestMachining
|
||||
|
||||
def taskPanelBaseLocationPage(self, obj, features):
|
||||
if not hasattr(self, "extensionsPanel"):
|
||||
|
||||
@@ -135,6 +135,7 @@ class ObjectPocket(PathPocketBase.ObjectPocket):
|
||||
|
||||
def opOnDocumentRestored(self, obj):
|
||||
"""opOnDocumentRestored(obj) ... adds the properties if they doesn't exist."""
|
||||
super().opOnDocumentRestored(obj)
|
||||
self.initPocketOp(obj)
|
||||
|
||||
def pocketInvertExtraOffset(self):
|
||||
|
||||
@@ -185,6 +185,25 @@ class ObjectPocket(PathAreaOp.ObjectOp):
|
||||
"Last Stepover Radius. If 0, 50% of cutter is used. Tuning this can be used to improve stepover for some shapes",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"UseRestMachining",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Skips machining regions that have already been cleared by previous operations.",
|
||||
),
|
||||
)
|
||||
obj.addProperty(
|
||||
"Part::PropertyPartShape",
|
||||
"RestMachiningRegions",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The areas cleared by this operation, one area per height, stored as a compound part. Used internally for rest machining.",
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("RestMachiningRegions", 2) # hide
|
||||
|
||||
for n in self.pocketPropertyEnumerations():
|
||||
setattr(obj, n[0], n[1])
|
||||
@@ -246,6 +265,42 @@ class ObjectPocket(PathAreaOp.ObjectOp):
|
||||
),
|
||||
)
|
||||
obj.PocketLastStepOver = 0
|
||||
|
||||
if not hasattr(obj, "UseRestMachining"):
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"UseRestMachining",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Skips machining regions that have already been cleared by previous operations.",
|
||||
),
|
||||
)
|
||||
|
||||
if not hasattr(obj, "RestMachiningRegions"):
|
||||
obj.addProperty(
|
||||
"Part::PropertyPartShape",
|
||||
"RestMachiningRegions",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"The areas cleared by this operation, one area per height, stored as a compound part. Used internally for rest machining.",
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("RestMachiningRegions", 2) # hide
|
||||
|
||||
obj.addProperty(
|
||||
"App::PropertyBool",
|
||||
"RestMachiningRegionsNeedRecompute",
|
||||
"Pocket",
|
||||
QT_TRANSLATE_NOOP(
|
||||
"App::Property",
|
||||
"Flag to indicate that the rest machining regions have never been computed, and must be recomputed before being used.",
|
||||
),
|
||||
)
|
||||
obj.setEditorMode("RestMachiningRegionsNeedRecompute", 2) # hide
|
||||
obj.RestMachiningRegionsNeedRecompute = True
|
||||
|
||||
Path.Log.track()
|
||||
|
||||
def areaOpPathParams(self, obj, isHole):
|
||||
|
||||
Reference in New Issue
Block a user