From 8dd767239cbeb65f06e459d759f6475ad5364c52 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 4 Jun 2020 13:39:55 -0500 Subject: [PATCH 1/3] Path: Add `ExpandProfile` feature Two new properties, `ExpandProfile` (length) and `ExpandProfileStepOver` (percent) added for new feature. New feature converts the normal single offset profile into a compound profile containing multiple increasing/decreasing offsets. The new feature clears a each layer completely before stepping down to the next layer. Adjust sorting procedure in PathAreaOp due to `ExpandProfile` modifications to open edges code. --- src/Mod/Path/PathScripts/PathAreaOp.py | 76 ++++--- src/Mod/Path/PathScripts/PathProfile.py | 274 ++++++++++++++++++++---- 2 files changed, 276 insertions(+), 74 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index f248fbfa44..067845c51d 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -29,14 +29,13 @@ import PathScripts.PathOp as PathOp import PathScripts.PathUtils as PathUtils import PathScripts.PathGeom as PathGeom import math +from PySide import QtCore # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader Draft = LazyLoader('Draft', globals(), 'Draft') Part = LazyLoader('Part', globals(), 'Part') -# from PathScripts.PathUtils import waiting_effects -from PySide import QtCore if FreeCAD.GuiUp: import FreeCADGui @@ -234,6 +233,8 @@ class ObjectOp(PathOp.ObjectOp): area.add(baseobject) areaParams = self.areaOpAreaParams(obj, isHole) # pylint: disable=assignment-from-no-return + if hasattr(obj, 'ExpandProfile') and obj.ExpandProfile != 0: + areaParams = self.areaOpAreaParamsExpandProfile(obj, isHole) # pylint: disable=assignment-from-no-return heights = [i for i in self.depthparams] PathLog.debug('depths: {}'.format(heights)) @@ -283,8 +284,8 @@ class ObjectOp(PathOp.ObjectOp): return pp, simobj - def _buildProfileOpenEdges(self, obj, baseShape, isHole, start, getsim): - '''_buildPathArea(obj, baseShape, isHole, start, getsim) ... internal function.''' + def _buildProfileOpenEdges(self, obj, edgeList, isHole, start, getsim): + '''_buildPathArea(obj, edgeList, isHole, start, getsim) ... internal function.''' # pylint: disable=unused-argument PathLog.track() @@ -293,35 +294,36 @@ class ObjectOp(PathOp.ObjectOp): PathLog.debug('depths: {}'.format(heights)) lstIdx = len(heights) - 1 for i in range(0, len(heights)): - hWire = Part.Wire(Part.__sortEdges__(baseShape.Edges)) - hWire.translate(FreeCAD.Vector(0, 0, heights[i] - hWire.BoundBox.ZMin)) + for baseShape in edgeList: + hWire = Part.Wire(Part.__sortEdges__(baseShape.Edges)) + hWire.translate(FreeCAD.Vector(0, 0, heights[i] - hWire.BoundBox.ZMin)) - pathParams = {} # pylint: disable=assignment-from-no-return - pathParams['shapes'] = [hWire] - pathParams['feedrate'] = self.horizFeed - pathParams['feedrate_v'] = self.vertFeed - pathParams['verbose'] = True - pathParams['resume_height'] = obj.SafeHeight.Value - pathParams['retraction'] = obj.ClearanceHeight.Value - pathParams['return_end'] = True - # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers - pathParams['preamble'] = False + pathParams = {} # pylint: disable=assignment-from-no-return + pathParams['shapes'] = [hWire] + pathParams['feedrate'] = self.horizFeed + pathParams['feedrate_v'] = self.vertFeed + pathParams['verbose'] = True + pathParams['resume_height'] = obj.SafeHeight.Value + pathParams['retraction'] = obj.ClearanceHeight.Value + pathParams['return_end'] = True + # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers + pathParams['preamble'] = False - if self.endVector is None: - V = hWire.Wires[0].Vertexes - lv = len(V) - 1 - pathParams['start'] = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) - if obj.Direction == 'CCW': - pathParams['start'] = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) - else: - pathParams['start'] = self.endVector + if self.endVector is None: + V = hWire.Wires[0].Vertexes + lv = len(V) - 1 + pathParams['start'] = FreeCAD.Vector(V[0].X, V[0].Y, V[0].Z) + if obj.Direction == 'CCW': + pathParams['start'] = FreeCAD.Vector(V[lv].X, V[lv].Y, V[lv].Z) + else: + pathParams['start'] = self.endVector - obj.PathParams = str({key: value for key, value in pathParams.items() if key != 'shapes'}) - PathLog.debug("Path with params: {}".format(obj.PathParams)) + obj.PathParams = str({key: value for key, value in pathParams.items() if key != 'shapes'}) + PathLog.debug("Path with params: {}".format(obj.PathParams)) - (pp, end_vector) = Path.fromShapes(**pathParams) - paths.extend(pp.Commands) - PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) + (pp, end_vector) = Path.fromShapes(**pathParams) + paths.extend(pp.Commands) + PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) self.endVector = end_vector # pylint: disable=attribute-defined-outside-init @@ -410,11 +412,17 @@ class ObjectOp(PathOp.ObjectOp): shapes.append(shp) if len(shapes) > 1: - jobs = [{ - 'x': s[0].BoundBox.XMax, - 'y': s[0].BoundBox.YMax, - 'shape': s - } for s in shapes] + jobs = list() + for s in shapes: + if s[2] == 'OpenEdge': + shp = Part.makeCompound(s[0]) + else: + shp = s[0] + jobs.append({ + 'x': shp.BoundBox.XMax, + 'y': shp.BoundBox.YMax, + 'shape': s + }) jobs = PathUtils.sort_jobs(jobs, ['x', 'y']) diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index ad4ac0536d..5c223334db 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -101,6 +101,10 @@ class ObjectProfile(PathAreaOp.ObjectOp): return [ ("App::PropertyEnumeration", "Direction", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)")), + ("App::PropertyLength", "ExpandProfile", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Extend the profile clearing beyond the Extra Offset.")), + ("App::PropertyPercent", "ExpandProfileStepOver", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), ("App::PropertyEnumeration", "HandleMultipleFeatures", "Profile", QtCore.QT_TRANSLATE_NOOP("PathPocket", "Choose how to process multiple Base Geometry features.")), ("App::PropertyEnumeration", "JoinType", "Profile", @@ -147,6 +151,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): return { 'AttemptInverseAngle': True, 'Direction': 'CW', + 'ExpandProfile': 0.0, + 'ExpandProfileStepOver': 100, 'HandleMultipleFeatures': 'Individually', 'InverseAngle': False, 'JoinType': 'Round', @@ -257,6 +263,29 @@ class ObjectProfile(PathAreaOp.ObjectOp): return params + def areaOpAreaParamsExpandProfile(self, obj, isHole): + '''areaOpPathParamsExpandProfile(obj, isHole) ... return dictionary with area parameters for expaned profile''' + params = {} + + params['Fill'] = 1 + params['Coplanar'] = 0 + params['PocketMode'] = 1 + params['SectionCount'] = -1 + # params['Angle'] = obj.ZigZagAngle + # params['FromCenter'] = (obj.StartAt == "Center") + params['PocketStepover'] = self.tool.Diameter * (float(obj.ExpandProfileStepOver) / 100.0) + extraOffset = obj.OffsetExtra.Value + if False: # self.pocketInvertExtraOffset(): # Method simply returns False + extraOffset = 0.0 - extraOffset + params['PocketExtraOffset'] = extraOffset + params['ToolRadius'] = self.radius + + # Pattern = ['ZigZag', 'Offset', 'Spiral', 'ZigZagOffset', 'Line', 'Grid', 'Triangle'] + params['PocketMode'] = 2 # Pattern.index(obj.OffsetPattern) + 1 + params['JoinType'] = 0 # jointype = ['Round', 'Square', 'Miter'] + + return params + def areaOpPathParams(self, obj, isHole): '''areaOpPathParams(obj, isHole) ... returns dictionary with path parameters. Do not overwrite.''' @@ -283,7 +312,9 @@ class ObjectProfile(PathAreaOp.ObjectOp): def areaOpUseProjection(self, obj): '''areaOpUseProjection(obj) ... returns True''' - return True + if obj.ExpandProfile.Value == 0.0: + return True + return False def opUpdateDepths(self, obj): if hasattr(obj, 'Base') and obj.Base.__len__() == 0: @@ -300,8 +331,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): edgeFaces = list() subCount = 0 self.inaccessibleMsg = translate('PathProfile', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') - self.profileshape = list() # pylint: disable=attribute-defined-outside-init - self.offsetExtra = obj.OffsetExtra.Value # abs(obj.OffsetExtra.Value) + self.offsetExtra = obj.OffsetExtra.Value + self.expandProfile = None if PathLog.getLevel(PathLog.thisModule()) == 4: for grpNm in ['tmpDebugGrp', 'tmpDebugGrp001']: @@ -313,6 +344,11 @@ class ObjectProfile(PathAreaOp.ObjectOp): tmpGrpNm = self.tmpGrp.Name self.JOB = PathUtils.findParentJob(obj) + if obj.ExpandProfile.Value != 0.0: + import PathScripts.PathSurfaceSupport as PathSurfaceSupport + self.PathSurfaceSupport = PathSurfaceSupport + self.expandProfile = True + if obj.UseComp: self.useComp = True self.ofstRadius = self.radius + self.offsetExtra @@ -389,56 +425,93 @@ class ObjectProfile(PathAreaOp.ObjectOp): startDepths.append(strDep) self.depthparams = self._customDepthParams(obj, strDep, finDep) - - for shape, wire in holes: f = Part.makeFace(wire, 'Part::FaceMakerSimple') - drillable = PathUtils.isDrillable(shape, wire) - if (drillable and obj.processCircles) or (not drillable and obj.processHoles): - env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams) - tup = env, True, 'pathProfileFaces', angle, axis, strDep, finDep - shapes.append(tup) + drillable = PathUtils.isDrillable(baseShape, wire) + ot = self._openingType(obj, baseShape, f, strDep, finDep) - if len(faces) > 0: - profileshape = Part.makeCompound(faces) - self.profileshape.append(profileshape) + if obj.processCircles: + if drillable: + if ot < 1: + cont = True + if obj.processHoles: + if not drillable: + if ot < 1: + cont = True + if cont: + if self.expandProfile: + shapeEnv = self._getExpandedProfileEnvelope(obj, f, True, obj.StartDepth.Value, finDep) + else: + shapeEnv = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams) + + if shapeEnv: + self._addDebugObject('HoleShapeEnvelope', shapeEnv) + # env = PathUtils.getEnvelope(baseShape, subshape=f, depthparams=self.depthparams) + tup = shapeEnv, True, 'pathProfile', angle, axis, strDep, finDep + shapes.append(tup) if obj.processPerimeter: if obj.HandleMultipleFeatures == 'Collectively': custDepthparams = self.depthparams + cont = True + + if len(faces) > 0: + profileshape = Part.makeCompound(faces) if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off': if profileshape.BoundBox.ZMin > obj.FinalDepth.Value: finDep = profileshape.BoundBox.ZMin - envDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope + custDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope + try: - env = PathUtils.getEnvelope(profileshape, depthparams=envDepthparams) + # env = PathUtils.getEnvelope(profileshape, depthparams=custDepthparams) + if self.expandProfile: + shapeEnv = self._getExpandedProfileEnvelope(obj, shape, False, obj.StartDepth.Value, finDep) + else: + shapeEnv = PathUtils.getEnvelope(profileshape, depthparams=custDepthparams) except Exception as ee: # pylint: disable=broad-except # PathUtils.getEnvelope() failed to return an object. msg = translate('Path', 'Unable to create path for face(s).') PathLog.error(msg + '\n{}'.format(ee)) - else: - tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep + cont = False + + if cont: + self._addDebugObject('CollectCutShapeEnv', shapeEnv) + tup = shapeEnv, False, 'pathProfile', angle, axis, strDep, finDep shapes.append(tup) elif obj.HandleMultipleFeatures == 'Individually': for shape in faces: finalDep = obj.FinalDepth.Value custDepthparams = self.depthparams + if obj.Side == 'Inside': if finalDep < shape.BoundBox.ZMin: # Recalculate depthparams finalDep = shape.BoundBox.ZMin custDepthparams = self._customDepthParams(obj, strDep + 0.5, finalDep) - env = PathUtils.getEnvelope(shape, depthparams=custDepthparams) - tup = env, False, 'pathProfileFaces', angle, axis, strDep, finalDep - shapes.append(tup) + if self.expandProfile: + shapeEnv = self._getExpandedProfileEnvelope(obj, shape, False, obj.StartDepth.Value, finalDep) + else: + shapeEnv = PathUtils.getEnvelope(shape, depthparams=custDepthparams) - else: # Try to build targets from the job base + if shapeEnv: + self._addDebugObject('IndivCutShapeEnv', shapeEnv) + tup = shapeEnv, False, 'pathProfile', angle, axis, strDep, finalDep + shapes.append(tup) + + else: # Try to build targets from the job models + # No base geometry selected, so treating operation like a exterior contour operation self.opUpdateDepths(obj) + obj.Side = 'Outside' # Force outside for whole model profile if 1 == len(self.model) and hasattr(self.model[0], "Proxy"): if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet + # Cancel ExpandProfile feature. Unavailable for ArchPanels. + if obj.ExpandProfile.Value != 0.0: + obj.ExpandProfile.Value == 0.0 + msg = translate('PathProfile', 'No ExpandProfile support for ArchPanel models.') + FreeCAD.Console.PrintWarning(msg + '\n') modelProxy = self.model[0].Proxy # Process circles and holes if requested by user if obj.processCircles or obj.processHoles: @@ -457,12 +530,15 @@ class ObjectProfile(PathAreaOp.ObjectOp): for wire in shape.Wires: f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) - tup = env, False, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = env, False, 'pathProfile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) else: - shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]) + # shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]) + PathLog.debug('Single model processed.') + shapes.extend(self._processEachModel(obj)) else: - shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]) + # shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]) + shapes.extend(self._processEachModel(obj)) self.removalshapes = shapes # pylint: disable=attribute-defined-outside-init PathLog.debug("%d shapes" % len(shapes)) @@ -529,6 +605,106 @@ class ObjectProfile(PathAreaOp.ObjectOp): return tup + def _openingType(self, obj, baseShape, face, strDep, finDep): + # Test if solid geometry above opening + extDistPos = strDep - face.BoundBox.ZMin + if extDistPos > 0: + extFacePos = face.extrude(FreeCAD.Vector(0.0, 0.0, extDistPos)) + cmnPos = baseShape.common(extFacePos) + if cmnPos.Volume > 0: + # Signifies solid protrusion above, + # or overhang geometry above opening + return 1 + # Test if solid geometry below opening + extDistNeg = finDep - face.BoundBox.ZMin + if extDistNeg < 0: + extFaceNeg = face.extrude(FreeCAD.Vector(0.0, 0.0, extDistNeg)) + cmnNeg = baseShape.common(extFaceNeg) + if cmnNeg.Volume == 0: + # No volume below signifies + # an unobstructed/nonconstricted opening through baseShape + return 0 + else: + # Could be a pocket, + # or a constricted/narrowing hole through baseShape + return -1 + msg = translate('PathProfile', 'failed to return opening type.') + PathLog.debug('_openingType() ' + msg) + return -2 + + # Method for expanded profile + def _getExpandedProfileEnvelope(self, obj, faceShape, isHole, strDep, finalDep): + shapeZ = faceShape.BoundBox.ZMin + + def calculateOffsetValue(obj, isHole): + offset = obj.ExpandProfile.Value + obj.OffsetExtra.Value # 0.0 + if obj.UseComp: + offset = obj.OffsetExtra.Value + self.tool.Diameter + offset += obj.ExpandProfile.Value + if isHole: + if obj.Side == 'Outside': + offset = 0 - offset + else: + if obj.Side == 'Inside': + offset = 0 - offset + return offset + + faceEnv = self.PathSurfaceSupport.getShapeEnvelope(faceShape) + # newFace = self.PathSurfaceSupport.getSliceFromEnvelope(faceEnv) + newFace = self.PathSurfaceSupport.getShapeSlice(faceEnv) + # Compute necessary offset + offsetVal = calculateOffsetValue(obj, isHole) + expandedFace = self.PathSurfaceSupport.extractFaceOffset(newFace, offsetVal, newFace) + if expandedFace: + if shapeZ != 0.0: + expandedFace.translate(FreeCAD.Vector(0.0, 0.0, shapeZ)) + newFace.translate(FreeCAD.Vector(0.0, 0.0, shapeZ)) + + if isHole: + if obj.Side == 'Outside': + newFace = newFace.cut(expandedFace) + else: + newFace = expandedFace.cut(newFace) + else: + if obj.Side == 'Inside': + newFace = newFace.cut(expandedFace) + else: + newFace = expandedFace.cut(newFace) + + if finalDep - shapeZ != 0: + newFace.translate(FreeCAD.Vector(0.0, 0.0, finalDep - shapeZ)) + + if strDep - finalDep != 0: + if newFace.Area > 0: + return newFace.extrude(FreeCAD.Vector(0.0, 0.0, strDep - finalDep)) + else: + PathLog.debug('No expanded profile face shape.\n') + return False + else: + PathLog.debug(translate('PathProfile', 'Failed to extract offset(s) for expanded profile.') + '\n') + + PathLog.debug(translate('PathProfile', 'Failed to expand profile.') + '\n') + return False + + # Method to handle each model as a whole, when no faces are selected + # It includes ExpandProfile implementation + def _processEachModel(self, obj): + shapeTups = list() + for base in self.model: + if hasattr(base, 'Shape'): + env = PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams) + if self.expandProfile: + eSlice = self.PathSurfaceSupport.getCrossSection(env) # getSliceFromEnvelope(env) + eSlice.translate(FreeCAD.Vector(0.0, 0.0, base.Shape.BoundBox.ZMin - env.BoundBox.ZMin)) + self._addDebugObject('ModelSlice', eSlice) + shapeEnv = self._getExpandedProfileEnvelope(obj, eSlice, False, obj.StartDepth.Value, obj.FinalDepth.Value) + else: + shapeEnv = env + + if shapeEnv: + shapeTups.append((shapeEnv, False)) + return shapeTups + # Edges pre-processing def _processEdges(self, obj): import DraftGeomUtils @@ -536,6 +712,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): basewires = list() delPairs = list() ezMin = None + self.cutOut = self.tool.Diameter * (float(obj.ExpandProfileStepOver) / 100.0) + for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] tmpSubs = list() @@ -570,10 +748,15 @@ class ObjectProfile(PathAreaOp.ObjectOp): zShift = ezMin - f.BoundBox.ZMin newPlace = FreeCAD.Placement(FreeCAD.Vector(0, 0, zShift), f.Placement.Rotation) f.Placement = newPlace - env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) - # shapes.append((env, False)) - tup = env, False, 'Profile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value - shapes.append(tup) + + if self.expandProfile: + shapeEnv = self._getExpandedProfileEnvelope(obj, Part.Face(f), False, obj.StartDepth.Value, ezMin) + else: + shapeEnv = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) + + if shapeEnv: + tup = shapeEnv, False, 'Profile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) else: PathLog.error(self.inaccessibleMsg) else: @@ -583,28 +766,39 @@ class ObjectProfile(PathAreaOp.ObjectOp): msg += translate('PathProfile', 'Please set to an acceptable value greater than zero.') PathLog.error(msg) else: - cutWireObjs = False flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) if flattened: + cutWireObjs = False + openEdges = list() + passOffsets = [self.ofstRadius] (origWire, flatWire) = flattened if PathLog.getLevel(PathLog.thisModule()) == 4: os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') os.Shape = flatWire os.purgeTouched() self.tmpGrp.addObject(os) + if self.expandProfile: + # Identify list of pass offset values for expanded profile paths + regularOfst = self.ofstRadius + targetOfst = regularOfst + obj.ExpandProfile.Value + while regularOfst < targetOfst: + regularOfst += self.cutOut + passOffsets.insert(0, regularOfst) - cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) - if cutShp: - cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) + for po in passOffsets: + self.ofstRadius = po + cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) + if cutShp: + cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) - if cutWireObjs: - for cW in cutWireObjs: - # shapes.append((cW, False)) - # self.profileEdgesIsOpen = True - tup = cW, False, 'OpenEdge', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value - shapes.append(tup) - else: - PathLog.error(self.inaccessibleMsg) + if cutWireObjs: + for cW in cutWireObjs: + openEdges.append(cW) + else: + PathLog.error(self.inaccessibleMsg) + + tup = openEdges, False, 'OpenEdge', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) else: PathLog.error(self.inaccessibleMsg) # Eif From 5f362e21a5caa922ae850d5b98270cf483295e5e Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Mon, 1 Jun 2020 14:56:14 -0500 Subject: [PATCH 2/3] Path: Cleanup and simplify code --- src/Mod/Path/PathScripts/PathProfile.py | 103 ++++++++++-------------- 1 file changed, 41 insertions(+), 62 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index 5c223334db..31a79e9195 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -41,10 +41,10 @@ ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') Part = LazyLoader('Part', globals(), 'Part') -__title__ = "Path Profile Faces Operation" +__title__ = "Path Profile Operation" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" -__doc__ = "Path Profile operation based on faces." +__doc__ = "Path Profile operation based on entire model, selected faces or selected edges." __contributors__ = "Schildkroet" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) @@ -246,7 +246,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): params['Coplanar'] = 0 params['SectionCount'] = -1 - offset = 0.0 + offset = obj.OffsetExtra.Value # 0.0 if obj.UseComp: offset = self.radius + obj.OffsetExtra.Value if obj.Side == 'Inside': @@ -330,11 +330,12 @@ class ObjectProfile(PathAreaOp.ObjectOp): allTuples = list() edgeFaces = list() subCount = 0 + self.isDebug = True if PathLog.getLevel(PathLog.thisModule()) == 4 else False self.inaccessibleMsg = translate('PathProfile', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') self.offsetExtra = obj.OffsetExtra.Value self.expandProfile = None - if PathLog.getLevel(PathLog.thisModule()) == 4: + if self.isDebug: for grpNm in ['tmpDebugGrp', 'tmpDebugGrp001']: if hasattr(FreeCAD.ActiveDocument, grpNm): for go in FreeCAD.ActiveDocument.getObject(grpNm).Group: @@ -373,7 +374,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): tup = self._analyzeFace(obj, base, sub, shape, subCount) allTuples.append(tup) - if subCount > 1: + if subCount > 1 and obj.HandleMultipleFeatures == 'Collectively': msg = translate('PathProfile', "Multiple faces in Base Geometry.") + " " msg += translate('PathProfile', "Depth settings will be applied to all faces.") FreeCAD.Console.PrintWarning(msg) @@ -389,7 +390,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): baseSubsTuples.append(pair) # Efor else: - PathLog.debug(translate("Path", "EnableRotation property is 'Off'.")) stock = PathUtils.findParentJob(obj).Stock for (base, subList) in obj.Base: baseSubsTuples.append((base, subList, 0.0, 'X', stock)) @@ -401,7 +401,6 @@ class ObjectProfile(PathAreaOp.ObjectOp): holes = [] faces = [] faceDepths = [] - startDepths = [] for sub in subsList: shape = getattr(base.Shape, sub) @@ -419,12 +418,12 @@ class ObjectProfile(PathAreaOp.ObjectOp): msg = translate('PathProfile', "Found a selected object which is not a face. Ignoring:") # FreeCAD.Console.PrintWarning(msg + " {}\n".format(ignoreSub)) - # Set initial Start and Final Depths and recalculate depthparams + # Identify initial Start and Final Depths finDep = obj.FinalDepth.Value strDep = obj.StartDepth.Value - startDepths.append(strDep) - self.depthparams = self._customDepthParams(obj, strDep, finDep) + for baseShape, wire in holes: + cont = False f = Part.makeFace(wire, 'Part::FaceMakerSimple') drillable = PathUtils.isDrillable(baseShape, wire) ot = self._openingType(obj, baseShape, f, strDep, finDep) @@ -521,7 +520,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): if (drillable and obj.processCircles) or (not drillable and obj.processHoles): f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) - tup = env, True, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + tup = env, True, 'pathProfile', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) # Process perimeter if requested by user @@ -544,7 +543,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): PathLog.debug("%d shapes" % len(shapes)) # Delete the temporary objects - if PathLog.getLevel(PathLog.thisModule()) == 4: + if self.isDebug: if FreeCAD.GuiUp: import FreeCADGui FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False @@ -739,6 +738,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): for base, wires in basewires: for wire in wires: if wire.isClosed(): + # Attempt to profile a closed wire + # f = Part.makeFace(wire, 'Part::FaceMakerSimple') # if planar error, Comment out previous line, uncomment the next two (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) @@ -772,11 +773,9 @@ class ObjectProfile(PathAreaOp.ObjectOp): openEdges = list() passOffsets = [self.ofstRadius] (origWire, flatWire) = flattened - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') - os.Shape = flatWire - os.purgeTouched() - self.tmpGrp.addObject(os) + + self._addDebugObject('FlatWire', flatWire) + if self.expandProfile: # Identify list of pass offset values for expanded profile paths regularOfst = self.ofstRadius @@ -905,13 +904,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Cut model(selected edges) from extended edges boundbox cutArea = extBndboxEXT.cut(base.Shape) - if PathLog.getLevel(PathLog.thisModule()) == 4: - CA = FCAD.addObject('Part::Feature', 'tmpCutArea') - CA.Shape = cutArea - CA.recompute() - CA.purgeTouched() - self.tmpGrp.addObject(CA) - + self._addDebugObject('CutArea', cutArea) # Get top and bottom faces of cut area (CA), and combine faces when necessary topFc = list() @@ -1032,12 +1025,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Add path stops at ends of wire cutShp = workShp.cut(pathStops) - if PathLog.getLevel(PathLog.thisModule()) == 4: - cs = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpCutShape') - cs.Shape = cutShp - cs.recompute() - cs.purgeTouched() - self.tmpGrp.addObject(cs) + self._addDebugObject('CutShape', cutShp) return cutShp @@ -1090,12 +1078,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): PathLog.error('No area to offset shape returned.\n{}'.format(ee)) return False - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') - os.Shape = ofstShp - os.recompute() - os.purgeTouched() - self.tmpGrp.addObject(os) + self._addDebugObject('OffsetShape', ofstShp) numOSWires = len(ofstShp.Wires) for w in range(0, numOSWires): @@ -1111,12 +1094,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): min0 = N[4] min0i = n (w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i - if PathLog.getLevel(PathLog.thisModule()) == 4: - near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0') - near0.Shape = Part.makeLine(cent0, pnt0) - near0.recompute() - near0.purgeTouched() - self.tmpGrp.addObject(near0) + near0Shp = Part.makeLine(cent0, pnt0) + self._addDebugObject('Near0', near0Shp) NEAR1 = self._findNearestVertex(ofstShp, cent1) min1i = 0 @@ -1127,17 +1106,13 @@ class ObjectProfile(PathAreaOp.ObjectOp): min1 = N[4] min1i = n (w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i - if PathLog.getLevel(PathLog.thisModule()) == 4: - near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1') - near1.Shape = Part.makeLine(cent1, pnt1) - near1.recompute() - near1.purgeTouched() - self.tmpGrp.addObject(near1) + near1Shp = Part.makeLine(cent1, pnt1) + self._addDebugObject('Near1', near1Shp) if w0 != w1: PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1)) - if False and PathLog.getLevel(PathLog.thisModule()) == 4: + if self.isDebug and False: PathLog.debug('min0i is {}.'.format(min0i)) PathLog.debug('min1i is {}.'.format(min1i)) PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) @@ -1206,12 +1181,13 @@ class ObjectProfile(PathAreaOp.ObjectOp): PathLog.debug('_extractFaceOffset()') areaParams = {} - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - if self.useComp is True: - offset = self.ofstRadius # + tolrnc - else: - offset = self.offsetExtra # + tolrnc + # JOB = PathUtils.findParentJob(obj) + # tolrnc = JOB.GeometryTolerance.Value + # if self.useComp: + # offset = self.ofstRadius # + tolrnc + # else: + # offset = self.offsetExtra # + tolrnc + offset = self.ofstRadius if isHole is False: offset = 0 - offset @@ -1385,7 +1361,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Efor # Eif - if False and PathLog.getLevel(PathLog.thisModule()) == 4: + # Remove `and False` when debugging open edges, as needed + if self.isDebug and False: PathLog.debug('grps[0]: {}'.format(grps[0])) PathLog.debug('grps[1]: {}'.format(grps[1])) PathLog.debug('wireIdxs[0]: {}'.format(wireIdxs[0])) @@ -1576,12 +1553,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): wire = Part.Wire([L1, L2, L3, L4, L5]) # Eif face = Part.Face(wire) - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl) - os.Shape = face - os.recompute() - os.purgeTouched() - self.tmpGrp.addObject(os) + self._addDebugObject(lbl, face) return face @@ -1616,6 +1588,13 @@ class ObjectProfile(PathAreaOp.ObjectOp): dist += elen return midPnt + # Method to add temporary debug object + def _addDebugObject(self, objName, objShape): + if self.isDebug: + O = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp_' + objName) + O.Shape = objShape + O.purgeTouched() + self.tmpGrp.addObject(O) def SetupProperties(): From e8ea6af98ff2809356affd943010986453d1ec61 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 18 Jun 2020 01:39:17 -0500 Subject: [PATCH 3/3] Path: Add operation's label to task panel window title --- src/Mod/Path/PathScripts/PathProfileGui.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index 57bbde4ff9..85efa959d4 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -51,6 +51,7 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): ''' def initPage(self, obj): + self.setTitle("Profile - " + obj.Label) self.updateVisibility() def profileFeatures(self):