diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui index bb14461cc4..45902b44ff 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpSurfaceEdit.ui @@ -7,7 +7,7 @@ 0 0 368 - 400 + 442 @@ -57,142 +57,24 @@ - - - - <html><head/><body><p>Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.</p></body></html> - - - - Planar - - - - - Rotational - - - - - - - - <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html> - - - - Single-pass - - - - - Multi-pass - - - - - - - - <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html> - - - 1 - - - 100 - - - 10 - - - 100 - - - - - + + - Step over + Cut Pattern - + Sample interval - - - - Layer Mode - - - - - - - <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html> - - - Optimize Linear Paths - - - - - - - Drop Cutter Direction - - - - - - - BoundBox extra offset X, Y - - - - - - - <html><head/><body><p>Make True, if specifying a Start Point</p></body></html> - - - Use Start Point - - - - - - - Scan Type - - - - - - - BoundBox - - - - - - - <html><head/><body><p>Set the Z-axis depth offset from the target surface.</p></body></html> - - - mm - - - - + - + 0 @@ -208,7 +90,7 @@ - + <html><head/><body><p>Additional offset to the selected bounding box along the Y axis."</p></body></html> @@ -219,8 +101,8 @@ - - + + <html><head/><body><p>Set the sampling resolution. Smaller values quickly increase processing time.</p></body></html> @@ -229,28 +111,21 @@ - - + + + + <html><head/><body><p>Enable optimization of linear paths (co-linear points). Removes unnecessary co-linear points from G-Code output.</p></body></html> + - Depth offset + Optimize Linear Paths - - - - <html><head/><body><p>Dropcutter lines are created parallel to this axis.</p></body></html> + + + + BoundBox - - - X - - - - - Y - - @@ -270,7 +145,7 @@ - + <html><head/><body><p>Enable separate optimization of transitions between, and breaks within, each step over path.</p></body></html> @@ -280,10 +155,37 @@ - - + + + + <html><head/><body><p>Profile the edges of the selection.</p></body></html> + + + + None + + + + + Only + + + + + First + + + + + Last + + + + + + - Cut Pattern + Step over @@ -324,6 +226,146 @@ + + + + <html><head/><body><p>Planar: Flat, 3D surface scan. Rotational: 4th-axis rotational scan.</p></body></html> + + + + Planar + + + + + Rotational + + + + + + + + BoundBox extra offset X, Y + + + + + + + Depth offset + + + + + + + <html><head/><body><p>Complete the operation in a single pass at depth, or mulitiple passes to final depth.</p></body></html> + + + + Single-pass + + + + + Multi-pass + + + + + + + + Layer Mode + + + + + + + Scan Type + + + + + + + <html><head/><body><p>Dropcutter lines are created parallel to this axis.</p></body></html> + + + + X + + + + + Y + + + + + + + + <html><head/><body><p>Set the Z-axis depth offset from the target surface.</p></body></html> + + + mm + + + + + + + Drop Cutter Direction + + + + + + + <html><head/><body><p>Make True, if specifying a Start Point</p></body></html> + + + Use Start Point + + + + + + + <html><head/><body><p>Avoid cutting the last 'N' faces in the Base Geometry list of selected faces.</p></body></html> + + + + + + + Profile Edges + + + + + + + Avoid Last X Faces + + + + + + + <html><head/><body><p>The amount by which the tool is laterally displaced on each cycle of the pattern, specified in percent of the tool diameter.</p><p>A step over of 100% results in no overlap between two different cycles.</p></body></html> + + + 1 + + + 100 + + + diff --git a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui index 82533fe061..412b32b6d0 100644 --- a/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui +++ b/src/Mod/Path/Gui/Resources/panels/PageOpWaterlineEdit.ui @@ -194,12 +194,6 @@ 100 - - 10 - - - 100 - diff --git a/src/Mod/Path/PathScripts/PathSurface.py b/src/Mod/Path/PathScripts/PathSurface.py index 9c6f2d3c0c..841e61d219 100644 --- a/src/Mod/Path/PathScripts/PathSurface.py +++ b/src/Mod/Path/PathScripts/PathSurface.py @@ -54,7 +54,6 @@ import math # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader -MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: @@ -72,18 +71,18 @@ def translate(context, text, disambig=None): class ObjectSurface(PathOp.ObjectOp): '''Proxy object for Surfacing operation.''' - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - def opFeatures(self, obj): - '''opFeatures(obj) ... return all standard features and edges based geometries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces + '''opFeatures(obj) ... return all standard features''' + return PathOp.FeatureTool | PathOp.FeatureDepths \ + | PathOp.FeatureHeights | PathOp.FeatureStepDown \ + | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces def initOperation(self, obj): - '''initPocketOp(obj) ... create operation specific properties''' - self.initOpProperties(obj) + '''initOperation(obj) ... Initialize the operation by + managing property creation and property editor status.''' + self.propertiesReady = False + + self.initOpProperties(obj) # Initialize operation-specific properties # For debugging if PathLog.getLevel(PathLog.thisModule()) != 4: @@ -94,28 +93,30 @@ class ObjectSurface(PathOp.ObjectOp): def initOpProperties(self, obj, warn=False): '''initOpProperties(obj) ... create operation specific properties''' - missing = list() + self.addNewProps = list() - for (prtyp, nm, grp, tt) in self.opProperties(): + for (prtyp, nm, grp, tt) in self.opPropertyDefinitions(): if not hasattr(obj, nm): obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - if warn: - newPropMsg = translate('PathSurface', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' - newPropMsg += translate('PathSurface', 'Check its default value.') - PathLog.warning(newPropMsg) + self.addNewProps.append(nm) # Set enumeration lists for enumeration properties - if len(missing) > 0: - ENUMS = self.propertyEnumerations() + if len(self.addNewProps) > 0: + ENUMS = self.opPropertyEnumerations() for n in ENUMS: - if n in missing: + if n in self.addNewProps: setattr(obj, n, ENUMS[n]) - self.addedAllProperties = True + if warn: + newPropMsg = translate('PathSurface', 'New property added to') + newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + '. ' + newPropMsg += translate('PathSurface', 'Check default value(s).') + FreeCAD.Console.PrintWarning(newPropMsg + '\n') - def opProperties(self): - '''opProperties(obj) ... Store operation specific properties''' + self.propertiesReady = True + + def opPropertyDefinitions(self): + '''opPropertyDefinitions(obj) ... Store operation specific properties''' return [ ("App::PropertyBool", "ShowTempObjects", "Debug", @@ -179,7 +180,7 @@ class ObjectSurface(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the edges of the selection.")), ("App::PropertyDistance", "SampleInterval", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), - ("App::PropertyPercent", "StepOver", "Clearing Options", + ("App::PropertyFloat", "StepOver", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", @@ -199,7 +200,7 @@ class ObjectSurface(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) ] - def propertyEnumerations(self): + def opPropertyEnumerations(self): # Enumeration lists for App::PropertyEnumeration properties return { 'BoundBox': ['BaseBoundBox', 'Stock'], @@ -214,6 +215,59 @@ class ObjectSurface(PathOp.ObjectOp): 'ScanType': ['Planar', 'Rotational'] } + def opPropertyDefaults(self, obj, job): + '''opPropertyDefaults(obj, job) ... returns a dictionary of default values + for the operation's properties.''' + defaults = { + 'OptimizeLinearPaths': True, + 'InternalFeaturesCut': True, + 'OptimizeStepOverTransitions': False, + 'CircularUseG2G3': False, + 'BoundaryEnforcement': True, + 'UseStartPoint': False, + 'AvoidLastX_InternalFeatures': True, + 'CutPatternReversed': False, + 'StartPoint': FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value), + 'ProfileEdges': 'None', + 'LayerMode': 'Single-pass', + 'ScanType': 'Planar', + 'RotationAxis': 'X', + 'CutMode': 'Conventional', + 'CutPattern': 'Line', + 'HandleMultipleFeatures': 'Collectively', + 'PatternCenterAt': 'CenterOfMass', + 'GapSizes': 'No gaps identified.', + 'StepOver': 100.0, + 'CutPatternAngle': 0.0, + 'CutterTilt': 0.0, + 'StartIndex': 0.0, + 'StopIndex': 360.0, + 'SampleInterval': 1.0, + 'BoundaryAdjustment': 0.0, + 'InternalFeaturesAdjustment': 0.0, + 'AvoidLastX_Faces': 0, + 'PatternCenterCustom': FreeCAD.Vector(0.0, 0.0, 0.0), + 'GapThreshold': 0.005, + 'AngularDeflection': 0.25, + 'LinearDeflection': 0.0001, + # For debugging + 'ShowTempObjects': False + } + + warn = True + if hasattr(job, 'GeometryTolerance'): + if job.GeometryTolerance.Value != 0.0: + warn = False + defaults['LinearDeflection'] = job.GeometryTolerance.Value + if warn: + msg = translate('PathSurface', + 'The GeometryTolerance for this Job is 0.0.') + msg += translate('PathSurface', + 'Initializing LinearDeflection to 0.0001 mm.') + FreeCAD.Console.PrintWarning(msg + '\n') + + return defaults + def setEditorProperties(self, obj): # Used to hide inputs in properties list @@ -241,23 +295,23 @@ class ObjectSurface(PathOp.ObjectOp): obj.setEditorMode('PatternCenterCustom', P2) def onChanged(self, obj, prop): - if hasattr(self, 'addedAllProperties'): - if self.addedAllProperties is True: - if prop == 'ScanType': - self.setEditorProperties(obj) - if prop == 'CutPattern': + if hasattr(self, 'propertiesReady'): + if self.propertiesReady: + if prop in ['ScanType', 'CutPattern']: self.setEditorProperties(obj) def opOnDocumentRestored(self, obj): - self.initOpProperties(obj, warn=True) + self.propertiesReady = False + job = PathUtils.findParentJob(obj) - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - else: - obj.setEditorMode('ShowTempObjects', 0) # show + self.initOpProperties(obj, warn=True) + self.opApplyPropertyDefaults(obj, job, self.addNewProps) + + mode = 2 if PathLog.getLevel(PathLog.thisModule()) != 4 else 0 + obj.setEditorMode('ShowTempObjects', mode) # Repopulate enumerations in case of changes - ENUMS = self.propertyEnumerations() + ENUMS = self.opPropertyEnumerations() for n in ENUMS: restore = False if hasattr(obj, n): @@ -269,51 +323,28 @@ class ObjectSurface(PathOp.ObjectOp): self.setEditorProperties(obj) + def opApplyPropertyDefaults(self, obj, job, propList): + # Set standard property defaults + PROP_DFLTS = self.opPropertyDefaults(obj, job) + for n in PROP_DFLTS: + if n in propList: + prop = getattr(obj, n) + val = PROP_DFLTS[n] + setVal = False + if hasattr(prop, 'Value'): + if isinstance(val, int) or isinstance(val, float): + setVal = True + if setVal: + propVal = getattr(prop, 'Value') + setattr(prop, 'Value', val) + else: + setattr(obj, n, val) + def opSetDefaultValues(self, obj, job): '''opSetDefaultValues(obj, job) ... initialize defaults''' job = PathUtils.findParentJob(obj) - obj.OptimizeLinearPaths = True - obj.InternalFeaturesCut = True - obj.OptimizeStepOverTransitions = False - obj.CircularUseG2G3 = False - obj.BoundaryEnforcement = True - obj.UseStartPoint = False - obj.AvoidLastX_InternalFeatures = True - obj.CutPatternReversed = False - obj.StartPoint.x = 0.0 - obj.StartPoint.y = 0.0 - obj.StartPoint.z = obj.ClearanceHeight.Value - obj.ProfileEdges = 'None' - obj.LayerMode = 'Single-pass' - obj.ScanType = 'Planar' - obj.RotationAxis = 'X' - obj.CutMode = 'Conventional' - obj.CutPattern = 'Line' - obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' - obj.GapSizes = 'No gaps identified.' - obj.StepOver = 100 - obj.CutPatternAngle = 0.0 - obj.CutterTilt = 0.0 - obj.StartIndex = 0.0 - obj.StopIndex = 360.0 - obj.SampleInterval.Value = 1.0 - obj.BoundaryAdjustment.Value = 0.0 - obj.InternalFeaturesAdjustment.Value = 0.0 - obj.AvoidLastX_Faces = 0 - obj.PatternCenterCustom.x = 0.0 - obj.PatternCenterCustom.y = 0.0 - obj.PatternCenterCustom.z = 0.0 - obj.GapThreshold.Value = 0.005 - obj.AngularDeflection.Value = 0.25 - obj.LinearDeflection.Value = job.GeometryTolerance.Value - # For debugging - obj.ShowTempObjects = False - - if job.GeometryTolerance.Value == 0.0: - PathLog.warning(translate('PathSurface', 'The GeometryTolerance for this Job is 0.0. Initializing LinearDeflection to 0.0001 mm.')) - obj.LinearDeflection.Value = 0.0001 + self.opApplyPropertyDefaults(obj, job, self.addNewProps) # need to overwrite the default depth calculations for facing d = None @@ -373,10 +404,10 @@ class ObjectSurface(PathOp.ObjectOp): PathLog.error(translate('PathSurface', 'Cut pattern angle limits are +- 360 degrees.')) # Limit StepOver to natural number percentage - if obj.StepOver > 100: - obj.StepOver = 100 - if obj.StepOver < 1: - obj.StepOver = 1 + if obj.StepOver > 100.0: + obj.StepOver = 100.0 + if obj.StepOver < 1.0: + obj.StepOver = 1.0 # Limit AvoidLastX_Faces to zero and positive values if obj.AvoidLastX_Faces < 0: @@ -403,6 +434,7 @@ class ObjectSurface(PathOp.ObjectOp): self.closedGap = False self.tmpCOM = None self.gaps = [0.1, 0.2, 0.3] + self.cancelOperation = False CMDS = list() modelVisibility = list() FCAD = FreeCAD.ActiveDocument @@ -423,7 +455,6 @@ class ObjectSurface(PathOp.ObjectOp): self.showDebugObjects = False # mark beginning of operation and identify parent Job - PathLog.info('\nBegin 3D Surface operation...') startTime = time.time() # Identify parent Job @@ -531,18 +562,18 @@ class ObjectSurface(PathOp.ObjectOp): PSF.radius = self.radius PSF.depthParams = self.depthParams pPM = PSF.preProcessModel(self.module) + # Process selected faces, if available - if pPM is False: - PathLog.error('Unable to pre-process obj.Base.') - else: + if pPM: + self.cancelOperation = False (FACES, VOIDS) = pPM self.modelSTLs = PSF.modelSTLs self.profileShapes = PSF.profileShapes - # Create OCL.stl model objects - self._prepareModelSTLs(JOB, obj) - for m in range(0, len(JOB.Model.Group)): + # Create OCL.stl model objects + PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, m, ocl) + Mdl = JOB.Model.Group[m] if FACES[m] is False: PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) @@ -553,7 +584,7 @@ class ObjectSurface(PathOp.ObjectOp): CMDS.append(Path.Command('G0', {'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid})) PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) # make stock-model-voidShapes STL model for avoidance detection on transitions - self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + PathSurfaceSupport._makeSafeSTL(self, JOB, obj, m, FACES[m], VOIDS[m], ocl) # Process model/faces - OCL objects must be ready CMDS.extend(self._processCutAreas(JOB, obj, m, FACES[m], VOIDS[m])) @@ -622,118 +653,22 @@ class ObjectSurface(PathOp.ObjectOp): del self.midDep execTime = time.time() - startTime - PathLog.info('Operation time: {} sec.'.format(execTime)) + if execTime > 60.0: + tMins = math.floor(execTime / 60.0) + tSecs = execTime - (tMins * 60.0) + exTime = str(tMins) + ' min. ' + str(round(tSecs, 5)) + ' sec.' + else: + exTime = str(round(execTime, 5)) + ' sec.' + FreeCAD.Console.PrintMessage('3D Surface operation time is {}\n'.format(exTime)) + + if self.cancelOperation: + FreeCAD.ActiveDocument.openTransaction(translate("PathSurface", "Canceled 3D Surface operation.")) + FreeCAD.ActiveDocument.removeObject(obj.Name) + FreeCAD.ActiveDocument.commitTransaction() return True - # Methods for constructing the cut area - def _prepareModelSTLs(self, JOB, obj): - PathLog.debug('_prepareModelSTLs()') - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - - # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") - if self.modelTypes[m] == 'M': - # TODO: test if this works - facets = M.Mesh.Facets.Points - else: - facets = Part.getFacets(M.Shape) - - if self.modelSTLs[m] is True: - stl = ocl.STLSurf() - - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - self.modelSTLs[m] = stl - return - - def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): - '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... - Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this - STL object to determine minimum travel height to clear stock and model.''' - PathLog.debug('_makeSafeSTL()') - - fuseShapes = list() - Mdl = JOB.Model.Group[mdlIdx] - mBB = Mdl.Shape.BoundBox - sBB = JOB.Stock.Shape.BoundBox - - # add Model shape to safeSTL shape - fuseShapes.append(Mdl.Shape) - - if obj.BoundBox == 'BaseBoundBox': - cont = False - extFwd = (sBB.ZLength) - zmin = mBB.ZMin - zmax = mBB.ZMin + extFwd - stpDwn = (zmax - zmin) / 4.0 - dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - - try: - envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as ee: - PathLog.error(str(ee)) - shell = Mdl.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as eee: - PathLog.error(str(eee)) - - if cont: - stckWst = JOB.Stock.Shape.cut(envBB) - if obj.BoundaryAdjustment > 0.0: - cmpndFS = Part.makeCompound(faceShapes) - baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape - adjStckWst = stckWst.cut(baBB) - else: - adjStckWst = stckWst - fuseShapes.append(adjStckWst) - else: - PathLog.warning('Path transitions might not avoid the model. Verify paths.') - else: - # If boundbox is Job.Stock, add hidden pad under stock as base plate - toolDiam = self.cutter.getDiameter() - zMin = JOB.Stock.Shape.BoundBox.ZMin - xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam - yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam - bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) - bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) - bH = 1.0 - crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) - B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) - fuseShapes.append(B) - - if voidShapes is not False: - voidComp = Part.makeCompound(voidShapes) - voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape - fuseShapes.append(voidEnv) - - fused = Part.makeCompound(fuseShapes) - - if self.showDebugObjects: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') - T.Shape = fused - T.purgeTouched() - self.tempGroup.addObject(T) - - facets = Part.getFacets(fused) - - stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - - self.safeSTLs[mdlIdx] = stl - + # Methods for constructing the cut area and creating path geometry def _processCutAreas(self, JOB, obj, mdlIdx, FCS, VDS): '''_processCutAreas(JOB, obj, mdlIdx, FCS, VDS)... This method applies any avoided faces or regions to the selected faces. @@ -784,10 +719,9 @@ class ObjectSurface(PathOp.ObjectOp): return final - # Methods for creating path geometry def _processPlanarOp(self, JOB, obj, mdlIdx, cmpdShp, fsi): - '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)... - This method compiles the main components for the procedural portion of a planar operation (non-rotational). + '''_processPlanarOp(JOB, obj, mdlIdx, cmpdShp)... + This method compiles the main components for the procedural portion of a planar operation (non-rotational). It creates the OCL PathDropCutter objects: model and safeTravel. It makes the necessary facial geometries for the actual cut area. It calls the correct Single or Multi-pass method as needed. diff --git a/src/Mod/Path/PathScripts/PathSurfaceGui.py b/src/Mod/Path/PathScripts/PathSurfaceGui.py index 7ff1342360..a26b290827 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceGui.py +++ b/src/Mod/Path/PathScripts/PathSurfaceGui.py @@ -141,11 +141,17 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): return signals - def updateVisibility(self): + def updateVisibility(self, sentObj=None): + '''updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects.''' if self.form.scanType.currentText() == 'Planar': self.form.cutPattern.show() self.form.cutPattern_label.show() self.form.optimizeStepOverTransitions.show() + if hasattr(self.form, 'profileEdges'): + self.form.profileEdges.show() + self.form.profileEdges_label.show() + self.form.avoidLastX_Faces.show() + self.form.avoidLastX_Faces_label.show() self.form.boundBoxExtraOffsetX.hide() self.form.boundBoxExtraOffsetY.hide() @@ -156,6 +162,11 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): self.form.cutPattern.hide() self.form.cutPattern_label.hide() self.form.optimizeStepOverTransitions.hide() + if hasattr(self.form, 'profileEdges'): + self.form.profileEdges.hide() + self.form.profileEdges_label.hide() + self.form.avoidLastX_Faces.hide() + self.form.avoidLastX_Faces_label.hide() self.form.boundBoxExtraOffsetX.show() self.form.boundBoxExtraOffsetY.show() diff --git a/src/Mod/Path/PathScripts/PathSurfaceSupport.py b/src/Mod/Path/PathScripts/PathSurfaceSupport.py index 0e0ca7cdfc..fc66645f1b 100644 --- a/src/Mod/Path/PathScripts/PathSurfaceSupport.py +++ b/src/Mod/Path/PathScripts/PathSurfaceSupport.py @@ -28,7 +28,6 @@ __title__ = "Path Surface Support Module" __author__ = "russ4262 (Russell Johnson)" __url__ = "http://www.freecadweb.org" __doc__ = "Support functions and classes for 3D Surface and Waterline operations." -# __name__ = "PathSurfaceSupport" __contributors__ = "" import FreeCAD @@ -37,7 +36,11 @@ import Path import PathScripts.PathLog as PathLog import PathScripts.PathUtils as PathUtils import math -import Part + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +# MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') +Part = LazyLoader('Part', globals(), 'Part') PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) @@ -54,7 +57,7 @@ class PathGeometryGenerator: PathGeometryGenerator(obj, shape, pattern) `obj` is the operation object, `shape` is the horizontal planar shape object, and `pattern` is the name of the geometric pattern to apply. - First, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center. + Frist, call the getCenterOfPattern() method for the CenterOfMass for patterns allowing a custom center. Next, call the generatePathGeometry() method to request the path geometry shape.''' # Register valid patterns here by name @@ -91,16 +94,16 @@ class PathGeometryGenerator: if shape.BoundBox.ZLength == 0.0: self.shape = shape else: - PathLog.warning('Shape appears to not be horizontal planar. ZMax is {}.'.format(shape.BoundBox.ZMax)) + FreeCAD.Console.PrintWarning('Shape appears to not be horizontal planar. ZMax is {}.\n'.format(shape.BoundBox.ZMax)) self._prepareConstants() def _prepareConstants(self): # Apply drop cutter extra offset and set the max and min XY area of the operation - xmin = self.shape.BoundBox.XMin - xmax = self.shape.BoundBox.XMax - ymin = self.shape.BoundBox.YMin - ymax = self.shape.BoundBox.YMax + # xmin = self.shape.BoundBox.XMin + # xmax = self.shape.BoundBox.XMax + # ymin = self.shape.BoundBox.YMin + # ymax = self.shape.BoundBox.YMax # Compute weighted center of mass of all faces combined if self.pattern in ['Circular', 'CircularZigZag', 'Spiral']: @@ -115,7 +118,8 @@ class PathGeometryGenerator: fCnt += 1 zeroCOM = zeroCOM.add(FreeCAD.Vector(comF.x, comF.y, 0.0).multiply(areaF)) if fCnt == 0: - PathLog.error(translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.')) + msg = translate(self.module, 'Cannot calculate the Center Of Mass. Using Center of Boundbox instead.') + FreeCAD.Console.PrintError(msg + '\n') bbC = self.shape.BoundBox.Center zeroCOM = FreeCAD.Vector(bbC.x, bbC.y, 0.0) else: @@ -152,11 +156,11 @@ class PathGeometryGenerator: '''generatePathGeometry()... Call this function to obtain the path geometry shape, generated by this class.''' if self.pattern == 'None': - PathLog.warning('PGG: No pattern set.') + # FreeCAD.Console.PrintWarning('PGG: No pattern set.\n') return False if self.shape is None: - PathLog.warning('PGG: No shape set.') + # FreeCAD.Console.PrintWarning('PGG: No shape set.\n') return False cmd = 'self._' + self.pattern + '()' @@ -229,17 +233,17 @@ class PathGeometryGenerator: cAng = math.atan(self.deltaX / self.deltaY) # BoundaryBox angle # Determine end points and create top lines - x1 = centRot.x - self.halfDiag - x2 = centRot.x + self.halfDiag - diag = None - if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180: - diag = self.deltaY - elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270: - diag = self.deltaX - else: - perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC - diag = perpDist - y1 = centRot.y + diag + # x1 = centRot.x - self.halfDiag + # x2 = centRot.x + self.halfDiag + # diag = None + # if self.obj.CutPatternAngle == 0 or self.obj.CutPatternAngle == 180: + # diag = self.deltaY + # elif self.obj.CutPatternAngle == 90 or self.obj.CutPatternAngle == 270: + # diag = self.deltaX + # else: + # perpDist = math.cos(cAng - math.radians(self.obj.CutPatternAngle)) * self.deltaC + # diag = perpDist + # y1 = centRot.y + diag # y2 = y1 # Create end points for set of lines to intersect with cross-section face @@ -404,8 +408,9 @@ class PathGeometryGenerator: while cont: ofstArea = self._getFaceOffset(shape, ofst) if not ofstArea: - PathLog.warning('PGG: No offset clearing area returned.') + # FreeCAD.Console.PrintWarning('PGG: No offset clearing area returned.\n') cont = False + True if cont else False # cont used for LGTM break for F in ofstArea.Faces: faces.append(F) @@ -470,7 +475,7 @@ class ProcessSelectedFaces: self.module = None self.radius = None self.depthParams = None - self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.') + self.msgNoFaces = translate(self.module, 'Face selection is unavailable for Rotational scans. Ignoring selected faces.') + '\n' self.JOB = JOB self.obj = obj self.profileEdges = 'None' @@ -493,7 +498,7 @@ class ProcessSelectedFaces: self.checkBase = True if self.obj.ScanType == 'Rotational': self.checkBase = False - PathLog.warning(self.msgNoFaces) + FreeCAD.Console.PrintWarning(self.msgNoFaces) def PathWaterline(self): if self.obj.Base: @@ -501,7 +506,7 @@ class ProcessSelectedFaces: self.checkBase = True if self.obj.Algorithm in ['OCL Dropcutter', 'Experimental']: self.checkBase = False - PathLog.warning(self.msgNoFaces) + FreeCAD.Console.PrintWarning(self.msgNoFaces) # public class methods def setShowDebugObjects(self, grpObj, val): @@ -520,6 +525,7 @@ class ProcessSelectedFaces: vShapes = list() GRP = self.JOB.Model.Group lenGRP = len(GRP) + proceed = False # Crete place holders for each base model in Job for m in range(0, lenGRP): @@ -532,16 +538,20 @@ class ProcessSelectedFaces: if self.checkBase: PathLog.debug(' -obj.Base exists. Pre-processing for selected faces.') - # (FACES, VOIDS) = self._identifyFacesAndVoids(FACES, VOIDS) - (F, V) = self._identifyFacesAndVoids(FACES, VOIDS) + (hasFace, hasVoid) = self._identifyFacesAndVoids(FACES, VOIDS) # modifies FACES and VOIDS + hasGeometry = True if hasFace or hasVoid else False # Cycle through each base model, processing faces for each for m in range(0, lenGRP): base = GRP[m] - (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, m, FACES, VOIDS) + (mFS, mVS, mPS) = self._preProcessFacesAndVoids(base, FACES[m], VOIDS[m]) fShapes[m] = mFS vShapes[m] = mVS self.profileShapes[m] = mPS + if mFS or mVS: + proceed = True + if hasGeometry and not proceed: + return False else: PathLog.debug(' -No obj.Base data.') for m in range(0, lenGRP): @@ -559,7 +569,7 @@ class ProcessSelectedFaces: pPEB = self._preProcessEntireBase(base, m) if pPEB is False: - PathLog.error(' -Failed to pre-process base as a whole.') + FreeCAD.Console.PrintError(' -Failed to pre-process base as a whole.\n') else: (fcShp, prflShp) = pPEB if fcShp is not False: @@ -609,6 +619,8 @@ class ProcessSelectedFaces: TUPS = list() GRP = self.JOB.Model.Group lenGRP = len(GRP) + hasFace = False + hasVoid = False # Separate selected faces into (base, face) tuples and flag model(s) for STL creation for (bs, SBS) in self.obj.Base: @@ -634,19 +646,21 @@ class ProcessSelectedFaces: if F[m] is False: F[m] = list() F[m].append((shape, faceIdx)) + hasFace = True else: if V[m] is False: V[m] = list() V[m].append((shape, faceIdx)) - return (F, V) + hasVoid = True + return (hasFace, hasVoid) - def _preProcessFacesAndVoids(self, base, m, FACES, VOIDS): + def _preProcessFacesAndVoids(self, base, FCS, VDS): mFS = False mVS = False mPS = False mIFS = list() - if FACES[m] is not False: + if FCS: isHole = False if self.obj.HandleMultipleFeatures == 'Collectively': cont = True @@ -654,26 +668,18 @@ class ProcessSelectedFaces: ifL = list() # avoid shape list outFCS = list() - # Get collective envelope slice of selected faces - for (fcshp, fcIdx) in FACES[m]: - fNum = fcIdx + 1 - fsL.append(fcshp) - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if self.obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) + # Use new face-unifying class + FUR = FindUnifiedRegions(FCS, self.JOB.GeometryTolerance.Value) + if self.showDebugObjects: + FUR.setTempGroup(self.tempGroup) + outFCS = FUR.getUnifiedRegions() + if not self.obj.InternalFeaturesCut: + ifL.extend(FUR.getInternalFeatures()) PathLog.debug('Attempting to get cross-section of collective faces.') if len(outFCS) == 0: - PathLog.error('Cannot process selected faces. Check horizontal surface exposure.'.format(fNum)) + msg = translate('PathSurfaceSupport', 'Cannot process selected faces. Check horizontal surface exposure.') + FreeCAD.Console.PrintError(msg + '\n') cont = False else: cfsL = Part.makeCompound(outFCS) @@ -688,7 +694,7 @@ class ProcessSelectedFaces: mFS = True cont = False else: - PathLog.error(' -Failed to create profile geometry for selected faces.') + # FreeCAD.Console.PrintError(' -Failed to create profile geometry for selected faces.\n') cont = False if cont: @@ -701,7 +707,7 @@ class ProcessSelectedFaces: ofstVal = self._calculateOffsetValue(isHole) faceOfstShp = extractFaceOffset(cfsL, ofstVal, self.wpc) if faceOfstShp is False: - PathLog.error(' -Failed to create offset face.') + FreeCAD.Console.PrintError(' -Failed to create offset face.\n') cont = False if cont: @@ -728,27 +734,19 @@ class ProcessSelectedFaces: # Eif elif self.obj.HandleMultipleFeatures == 'Individually': - for (fcshp, fcIdx) in FACES[m]: + for (fcshp, fcIdx) in FCS: cont = True ifL = list() # avoid shape list fNum = fcIdx + 1 outerFace = False - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from Face{}'.format(fNum)) - cont = False - elif gFW[0] is False: - PathLog.debug('Cannot process Face{}. Check that it has horizontal surface exposure.'.format(fNum)) - cont = False - outerFace = False - else: - ((otrFace, raised), intWires) = gFW - outerFace = otrFace - if self.obj.InternalFeaturesCut is False: - if intWires is not False: - for (iFace, rsd) in intWires: - ifL.append(iFace) + # Use new face-unifying class + FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value) + if self.showDebugObjects: + FUR.setTempGroup(self.tempGroup) + outerFace = FUR.getUnifiedRegions()[0] + if not self.obj.InternalFeaturesCut: + ifL = FUR.getInternalFeatures() if outerFace is not False: PathLog.debug('Attempting to create offset face of Face{}'.format(fNum)) @@ -766,7 +764,7 @@ class ProcessSelectedFaces: mFS.append(True) cont = False else: - PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) + # PathLog.error(' -Failed to create profile geometry for Face{}.'.format(fNum)) cont = False if cont: @@ -799,26 +797,23 @@ class ProcessSelectedFaces: for ifs in mIFS: mVS.append(ifs) - if VOIDS[m] is not False: + if VDS is not False: PathLog.debug('Processing avoid faces.') cont = True isHole = False outFCS = list() intFEAT = list() - for (fcshp, fcIdx) in VOIDS[m]: + for (fcshp, fcIdx) in VDS: fNum = fcIdx + 1 - gFW = self._getFaceWires(base, fcshp, fcIdx) - if gFW is False: - PathLog.debug('Failed to get wires from avoid Face{}'.format(fNum)) - cont = False - else: - ((otrFace, raised), intWires) = gFW - outFCS.append(otrFace) - if self.obj.AvoidLastX_InternalFeatures is False: - if intWires is not False: - for (iFace, rsd) in intWires: - intFEAT.append(iFace) + + # Use new face-unifying class + FUR = FindUnifiedRegions([(fcshp, fcIdx)], self.JOB.GeometryTolerance.Value) + if self.showDebugObjects: + FUR.setTempGroup(self.tempGroup) + outFCS.extend(FUR.getUnifiedRegions()) + if not self.obj.InternalFeaturesCut: + intFEAT.extend(FUR.getInternalFeatures()) lenOtFcs = len(outFCS) if lenOtFcs == 0: @@ -846,7 +841,7 @@ class ProcessSelectedFaces: ofstVal = self._calculateOffsetValue(isHole, isVoid=True) avdOfstShp = extractFaceOffset(avoid, ofstVal, self.wpc) if avdOfstShp is False: - PathLog.error('Failed to create collective offset avoid face.') + FreeCAD.Console.PrintError('Failed to create collective offset avoid face.\n') cont = False if cont: @@ -860,7 +855,7 @@ class ProcessSelectedFaces: ofstVal = self._calculateOffsetValue(isHole=True) ifOfstShp = extractFaceOffset(ifc, ofstVal, self.wpc) if ifOfstShp is False: - PathLog.error('Failed to create collective offset avoid internal features.') + FreeCAD.Console.PrintError('Failed to create collective offset avoid internal features.\n') else: avdShp = avdOfstShp.cut(ifOfstShp) @@ -868,44 +863,8 @@ class ProcessSelectedFaces: mVS = list() mVS.append(avdShp) - return (mFS, mVS, mPS) - def _getFaceWires(self, base, fcshp, fcIdx): - outFace = False - INTFCS = list() - fNum = fcIdx + 1 - warnFinDep = translate(self.module, 'Final Depth might need to be lower. Internal features detected in Face') - - PathLog.debug('_getFaceWires() from Face{}'.format(fNum)) - WIRES = self._extractWiresFromFace(base, fcshp) - if WIRES is False: - PathLog.error('Failed to extract wires from Face{}'.format(fNum)) - return False - - # Process remaining internal features, adding to FCS list - lenW = len(WIRES) - for w in range(0, lenW): - (wire, rsd) = WIRES[w] - PathLog.debug('Processing Wire{} in Face{}. isRaised: {}'.format(w + 1, fNum, rsd)) - if wire.isClosed() is False: - PathLog.debug(' -wire is not closed.') - else: - slc = self._flattenWireToFace(wire) - if slc is False: - PathLog.error('FAILED to identify horizontal exposure on Face{}.'.format(fNum)) - else: - if w == 0: - outFace = (slc, rsd) - else: - # add to VOIDS so cutter avoids area. - PathLog.warning(warnFinDep + str(fNum) + '.') - INTFCS.append((slc, rsd)) - if len(INTFCS) == 0: - return (outFace, False) - else: - return (outFace, INTFCS) - def _preProcessEntireBase(self, base, m): cont = True isHole = False @@ -928,10 +887,8 @@ class ProcessSelectedFaces: if cont: csFaceShape = getShapeSlice(baseEnv) if csFaceShape is False: - PathLog.debug('getShapeSlice(baseEnv) failed') csFaceShape = getCrossSection(baseEnv) if csFaceShape is False: - PathLog.debug('getCrossSection(baseEnv) failed') csFaceShape = getSliceFromEnvelope(baseEnv) if csFaceShape is False: PathLog.error('Failed to slice baseEnv shape.') @@ -946,103 +903,19 @@ class ProcessSelectedFaces: return (True, psOfst) prflShp = psOfst else: - PathLog.error(' -Failed to create profile geometry.') + # FreeCAD.Console.PrintError(' -Failed to create profile geometry.\n') cont = False if cont: ofstVal = self._calculateOffsetValue(isHole) faceOffsetShape = extractFaceOffset(csFaceShape, ofstVal, self.wpc) if faceOffsetShape is False: - PathLog.error('extractFaceOffset() failed.') + PathLog.error('extractFaceOffset() failed for entire base.') else: faceOffsetShape.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - faceOffsetShape.BoundBox.ZMin)) return (faceOffsetShape, prflShp) return False - def _extractWiresFromFace(self, base, fc): - '''_extractWiresFromFace(base, fc) ... - Attempts to return all closed wires within a parent face, including the outer most wire of the parent. - The wires are ordered by area. Each wire is also categorized as a pocket(False) or raised protrusion(True). - ''' - PathLog.debug('_extractWiresFromFace()') - - WIRES = list() - lenWrs = len(fc.Wires) - PathLog.debug(' -Wire count: {}'.format(lenWrs)) - - def index0(tup): - return tup[0] - - # Cycle through wires in face - for w in range(0, lenWrs): - PathLog.debug(' -Analyzing wire_{}'.format(w + 1)) - wire = fc.Wires[w] - checkEdges = False - cont = True - - # Check for closed edges (circles, ellipses, etc...) - for E in wire.Edges: - if E.isClosed() is True: - checkEdges = True - break - - if checkEdges is True: - PathLog.debug(' -checkEdges is True') - for e in range(0, len(wire.Edges)): - edge = wire.Edges[e] - if edge.isClosed() is True and edge.Mass > 0.01: - PathLog.debug(' -Found closed edge') - raised = False - ip = self._isPocket(base, fc, edge) - if ip is False: - raised = True - ebb = edge.BoundBox - eArea = ebb.XLength * ebb.YLength - F = Part.Face(Part.Wire([edge])) - WIRES.append((eArea, F.Wires[0], raised)) - cont = False - - if cont: - PathLog.debug(' -cont is True') - # If only one wire and not checkEdges, return first wire - if lenWrs == 1: - return [(wire, False)] - - raised = False - wbb = wire.BoundBox - wArea = wbb.XLength * wbb.YLength - if w > 0: - ip = self._isPocket(base, fc, wire) - if ip is False: - raised = True - WIRES.append((wArea, Part.Wire(wire.Edges), raised)) - - nf = len(WIRES) - if nf > 0: - PathLog.debug(' -number of wires found is {}'.format(nf)) - if nf == 1: - (area, W, raised) = WIRES[0] - owLen = fc.OuterWire.Length - wLen = W.Length - if abs(owLen - wLen) > 0.0000001: - OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) - return [(OW, False), (W, raised)] - else: - return [(W, raised)] - else: - sortedWIRES = sorted(WIRES, key=index0, reverse=True) - WRS = [(W, raised) for (area, W, raised) in sortedWIRES] # outer, then inner by area size - # Check if OuterWire is larger than largest in WRS list - (W, raised) = WRS[0] - owLen = fc.OuterWire.Length - wLen = W.Length - if abs(owLen - wLen) > 0.0000001: - OW = Part.Wire(Part.__sortEdges__(fc.OuterWire.Edges)) - WRS.insert(0, (OW, False)) - return WRS - - return False - def _calculateOffsetValue(self, isHole, isVoid=False): '''_calculateOffsetValue(self.obj, isHole, isVoid) ... internal function. Calculate the offset for the Path.Area() function.''' @@ -1066,75 +939,6 @@ class ProcessSelectedFaces: return offset - def _isPocket(self, b, f, w): - '''_isPocket(b, f, w)... - Attempts to determine if the wire(w) in face(f) of base(b) is a pocket or raised protrusion. - Returns True if pocket, False if raised protrusion.''' - e = w.Edges[0] - for fi in range(0, len(b.Shape.Faces)): - face = b.Shape.Faces[fi] - for ei in range(0, len(face.Edges)): - edge = face.Edges[ei] - if e.isSame(edge) is True: - if f is face: - # Alternative: run loop to see if all edges are same - pass # same source face, look for another - else: - if face.CenterOfMass.z < f.CenterOfMass.z: - return True - return False - - def _flattenWireToFace(self, wire): - PathLog.debug('_flattenWireToFace()') - if wire.isClosed() is False: - PathLog.debug(' -wire.isClosed() is False') - return False - - # If wire is planar horizontal, convert to a face and return - if wire.BoundBox.ZLength == 0.0: - slc = Part.Face(wire) - return slc - - # Attempt to create a new wire for manipulation, if not, use original - newWire = Part.Wire(wire.Edges) - if newWire.isClosed() is True: - nWire = newWire - else: - PathLog.debug(' -newWire.isClosed() is False') - nWire = wire - - # Attempt extrusion, and then try a manual slice and then cross-section - ext = getExtrudedShape(nWire) - if ext is False: - PathLog.debug('getExtrudedShape() failed') - else: - slc = getShapeSlice(ext) - if slc is not False: - return slc - cs = getCrossSection(ext, True) - if cs is not False: - return cs - - # Attempt creating an envelope, and then try a manual slice and then cross-section - env = getShapeEnvelope(nWire) - if env is False: - PathLog.debug('getShapeEnvelope() failed') - else: - slc = getShapeSlice(env) - if slc is not False: - return slc - cs = getCrossSection(env, True) - if cs is not False: - return cs - - # Attempt creating a projection - slc = getProjectedFace(self.tempGroup, nWire) - if slc is False: - PathLog.debug('getProjectedFace() failed') - else: - return slc - - return False # Eclass @@ -1153,6 +957,7 @@ def getExtrudedShape(wire): SHP = Part.makeSolid(shell) return SHP + def getShapeSlice(shape): PathLog.debug('getShapeSlice()') @@ -1198,10 +1003,9 @@ def getShapeSlice(shape): comp = Part.makeCompound(fL) return comp - # PathLog.debug(' -slcArea !< midArea') - # PathLog.debug(' -slcShp.Edges count: {}. Might be a vertically oriented face.'.format(len(slcShp.Edges))) return False + def getProjectedFace(tempGroup, wire): import Draft PathLog.debug('getProjectedFace()') @@ -1226,7 +1030,8 @@ def getProjectedFace(tempGroup, wire): slc.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - slc.BoundBox.ZMin)) return slc -def getCrossSection(shape, withExtrude=False): + +def getCrossSection(shape): PathLog.debug('getCrossSection()') wires = list() bb = shape.BoundBox @@ -1242,13 +1047,7 @@ def getCrossSection(shape, withExtrude=False): if csWire.isClosed() is False: PathLog.debug(' -comp.Wires[0] is not closed') return False - if withExtrude is True: - ext = getExtrudedShape(csWire) - CS = getShapeSlice(ext) - if CS is False: - return False - else: - CS = Part.Face(csWire) + CS = Part.Face(csWire) CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) return CS else: @@ -1256,6 +1055,7 @@ def getCrossSection(shape, withExtrude=False): return False + def getShapeEnvelope(shape): PathLog.debug('getShapeEnvelope()') @@ -1269,11 +1069,12 @@ def getShapeEnvelope(shape): try: env = PathUtils.getEnvelope(partshape=shape, depthparams=dep_par) # Produces .Shape except Exception as ee: - PathLog.error('try: PathUtils.getEnvelope() failed.\n' + str(ee)) + FreeCAD.Console.PrintError('try: PathUtils.getEnvelope() failed.\n' + str(ee) + '\n') return False else: return env + def getSliceFromEnvelope(env): PathLog.debug('getSliceFromEnvelope()') eBB = env.BoundBox @@ -1338,6 +1139,125 @@ def extractFaceOffset(fcShape, offset, wpc, makeComp=True): return ofstFace # offsetShape +# Functions for making model STLs +def _prepareModelSTLs(self, JOB, obj, m, ocl): + PathLog.debug('_prepareModelSTLs()') + import MeshPart + + if self.modelSTLs[m] is True: + M = JOB.Model.Group[m] + + # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") + if self.modelTypes[m] == 'M': + # TODO: test if this works + facets = M.Mesh.Facets.Points + else: + facets = Part.getFacets(M.Shape) + # mesh = MeshPart.meshFromShape(Shape=M.Shape, + # LinearDeflection=obj.LinearDeflection.Value, + # AngularDeflection=obj.AngularDeflection.Value, + # Relative=False) + + stl = ocl.STLSurf() + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + self.modelSTLs[m] = stl + return + + +def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes, ocl): + '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... + Creates and OCL.stl object with combined data with waste stock, + model, and avoided faces. Travel lines can be checked against this + STL object to determine minimum travel height to clear stock and model.''' + PathLog.debug('_makeSafeSTL()') + import MeshPart + + fuseShapes = list() + Mdl = JOB.Model.Group[mdlIdx] + mBB = Mdl.Shape.BoundBox + sBB = JOB.Stock.Shape.BoundBox + + # add Model shape to safeSTL shape + fuseShapes.append(Mdl.Shape) + + if obj.BoundBox == 'BaseBoundBox': + cont = False + extFwd = (sBB.ZLength) + zmin = mBB.ZMin + zmax = mBB.ZMin + extFwd + stpDwn = (zmax - zmin) / 4.0 + dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) + + try: + envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as ee: + PathLog.error(str(ee)) + shell = Mdl.Shape.Shells[0] + solid = Part.makeSolid(shell) + try: + envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape + cont = True + except Exception as eee: + PathLog.error(str(eee)) + + if cont: + stckWst = JOB.Stock.Shape.cut(envBB) + if obj.BoundaryAdjustment > 0.0: + cmpndFS = Part.makeCompound(faceShapes) + baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape + adjStckWst = stckWst.cut(baBB) + else: + adjStckWst = stckWst + fuseShapes.append(adjStckWst) + else: + PathLog.warning('Path transitions might not avoid the model. Verify paths.') + else: + # If boundbox is Job.Stock, add hidden pad under stock as base plate + toolDiam = self.cutter.getDiameter() + zMin = JOB.Stock.Shape.BoundBox.ZMin + xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam + yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam + bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) + bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) + bH = 1.0 + crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) + B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) + fuseShapes.append(B) + + if voidShapes is not False: + voidComp = Part.makeCompound(voidShapes) + voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape + fuseShapes.append(voidEnv) + + fused = Part.makeCompound(fuseShapes) + + if self.showDebugObjects: + T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') + T.Shape = fused + T.purgeTouched() + self.tempGroup.addObject(T) + + facets = Part.getFacets(fused) + # mesh = MeshPart.meshFromShape(Shape=fused, + # LinearDeflection=obj.LinearDeflection.Value, + # AngularDeflection=obj.AngularDeflection.Value, + # Relative=False) + + stl = ocl.STLSurf() + for tri in facets: + t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), + ocl.Point(tri[1][0], tri[1][1], tri[1][2]), + ocl.Point(tri[2][0], tri[2][1], tri[2][2])) + stl.addTriangle(t) + + self.safeSTLs[mdlIdx] = stl + + # Functions to convert path geometry into line/arc segments for OCL input or directly to g-code def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps): '''pathGeomToLinesPointSet(obj, compGeoShp)... @@ -1404,6 +1324,7 @@ def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps (vA, vB) = inLine.pop() # pop off previous line segment for combining with current tup = (vA, tup[1]) closedGap = True + True if closedGap else False # used closedGap for LGTM else: # PathLog.debug('---- Gap: {} mm'.format(gap)) gap = round(gap, 6) @@ -1411,6 +1332,7 @@ def pathGeomToLinesPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gaps gaps.insert(0, gap) gaps.pop() inLine.append(tup) + # Efor lnCnt += 1 if cutClimb is True: @@ -1450,11 +1372,10 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap lnCnt = 0 chkGap = False ec = len(compGeoShp.Edges) + dirFlg = 1 - if cutClimb is True: + if cutClimb: dirFlg = -1 - else: - dirFlg = 1 edg0 = compGeoShp.Edges[0] p1 = (edg0.Vertexes[0].X, edg0.Vertexes[0].Y) @@ -1476,9 +1397,8 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap cp = FreeCAD.Vector(v1[0], v1[1], 0.0) # check point (start point of segment) ep = FreeCAD.Vector(v2[0], v2[1], 0.0) # end point - # iC = sp.isOnLineSegment(ep, cp) iC = cp.isOnLineSegment(sp, ep) - if iC is True: + if iC: inLine.append('BRK') chkGap = True gap = abs(toolDiam - lst.sub(cp).Length) @@ -1486,7 +1406,6 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap chkGap = False if dirFlg == -1: inLine.reverse() - # LINES.append((dirFlg, inLine)) LINES.append(inLine) lnCnt += 1 dirFlg = -1 * dirFlg # Change zig to zag @@ -1499,7 +1418,7 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap else: tup = (v2, v1) - if chkGap is True: + if chkGap: if gap < obj.GapThreshold.Value: b = inLine.pop() # pop off 'BRK' marker (vA, vB) = inLine.pop() # pop off previous line segment for combining with current @@ -1524,8 +1443,8 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap else: PathLog.debug('Line count is ODD.') dirFlg = -1 * dirFlg - if obj.CutPatternReversed is False: - if cutClimb is True: + if not obj.CutPatternReversed: + if cutClimb: dirFlg = -1 * dirFlg if obj.CutPatternReversed: @@ -1553,11 +1472,8 @@ def pathGeomToZigzagPointSet(obj, compGeoShp, cutClimb, toolDiam, closedGap, gap rev2.append((p2, p1)) rev2.reverse() rev = rev2 - - # LINES.append((dirFlg, rev)) LINES.append(rev) else: - # LINES.append((dirFlg, inLine)) LINES.append(inLine) return LINES @@ -1783,7 +1699,6 @@ def pathGeomToSpiralPointSet(obj, compGeoShp): p2 = FreeCAD.Vector(edg1.Vertexes[1].X, edg1.Vertexes[1].Y, 0.0) tup = ((p1.x, p1.y), (p2.x, p2.y)) inLine.append(tup) - lst = p2 for ei in range(start, ec): # Skipped first edge, started with second edge above as edg1 edg = compGeoShp.Edges[ei] # Get edge for vertexes @@ -1798,7 +1713,7 @@ def pathGeomToSpiralPointSet(obj, compGeoShp): lnCnt += 1 inLine = list() # reset container inLine.append(tup) - p1 = sp + # p1 = sp p2 = ep # Efor @@ -1852,4 +1767,520 @@ def pathGeomToOffsetPointSet(obj, compGeoShp): LINES.append(tup) # Efor - return [LINES] \ No newline at end of file + return [LINES] + + +class FindUnifiedRegions: + '''FindUnifiedRegions() This class requires a list of face shapes. + It finds the unified horizontal unified regions, if they exist.''' + + def __init__(self, facesList, geomToler): + self.FACES = facesList # format is tuple (faceShape, faceIndex_on_base) + self.geomToler = geomToler + self.tempGroup = None + self.topFaces = list() + self.edgeData = list() + self.circleData = list() + self.noSharedEdges = True + self.topWires = list() + self.REGIONS = list() + self.INTERNALS = False + self.idGroups = list() + self.sharedEdgeIdxs = list() + self.fusedFaces = None + + if self.geomToler == 0.0: + self.geomToler = 0.00001 + + # Internal processing methods + def _showShape(self, shape, name): + if self.tempGroup: + S = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + name) + S.Shape = shape + S.purgeTouched() + self.tempGroup.addObject(S) + + def _extractTopFaces(self): + for (F, fcIdx) in self.FACES: # format is tuple (faceShape, faceIndex_on_base) + cont = True + fNum = fcIdx + 1 + # Extrude face + fBB = F.BoundBox + extFwd = math.floor(2.0 * fBB.ZLength) + 10.0 + ef = F.extrude(FreeCAD.Vector(0.0, 0.0, extFwd)) + ef = Part.makeSolid(ef) + + # Cut top off of extrusion with Part.box + efBB = ef.BoundBox + ZLen = efBB.ZLength / 2.0 + cutBox = Part.makeBox(efBB.XLength + 2.0, efBB.YLength + 2.0, ZLen) + zHght = efBB.ZMin + ZLen + cutBox.translate(FreeCAD.Vector(efBB.XMin - 1.0, efBB.YMin - 1.0, zHght)) + base = ef.cut(cutBox) + + # Identify top face of base + fIdx = 0 + zMin = base.Faces[fIdx].BoundBox.ZMin + for bfi in range(0, len(base.Faces)): + fzmin = base.Faces[bfi].BoundBox.ZMin + if fzmin > zMin: + fIdx = bfi + zMin = fzmin + + # Translate top face to Z=0.0 and save to topFaces list + topFace = base.Faces[fIdx] + # self._showShape(topFace, 'topFace_{}'.format(fNum)) + tfBB = topFace.BoundBox + tfBB_Area = tfBB.XLength * tfBB.YLength + fBB_Area = fBB.XLength * fBB.YLength + if tfBB_Area < (fBB_Area * 0.9): + # attempt alternate methods + topFace = self._getCompleteCrossSection(ef) + tfBB = topFace.BoundBox + tfBB_Area = tfBB.XLength * tfBB.YLength + # self._showShape(topFace, 'topFaceAlt_1_{}'.format(fNum)) + if tfBB_Area < (fBB_Area * 0.9): + topFace = getShapeSlice(ef) + tfBB = topFace.BoundBox + tfBB_Area = tfBB.XLength * tfBB.YLength + # self._showShape(topFace, 'topFaceAlt_2_{}'.format(fNum)) + if tfBB_Area < (fBB_Area * 0.9): + FreeCAD.Console.PrintError('Faild to extract processing region for Face{}.\n'.format(fNum)) + cont = False + + if cont: + topFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - zMin)) + self.topFaces.append((topFace, fcIdx)) + + def _fuseTopFaces(self): + (one, baseFcIdx) = self.topFaces.pop(0) + base = one + for (face, fcIdx) in self.topFaces: + base = base.fuse(face) + self.topFaces.insert(0, (one, baseFcIdx)) + self.fusedFaces = base + + def _getEdgesData(self): + topFaces = self.fusedFaces.Faces + tfLen = len(topFaces) + count = [0, 0] + + # Get length and center of mass for each edge in all top faces + for fi in range(0, tfLen): + F = topFaces[fi] + edgCnt = len(F.Edges) + for ei in range(0, edgCnt): + E = F.Edges[ei] + tup = (E.Length, E.CenterOfMass, E, fi) + if len(E.Vertexes) == 1: + self.circleData.append(tup) + count[0] += 1 + else: + self.edgeData.append(tup) + count[1] += 1 + + def _groupEdgesByLength(self): + cont = True + threshold = self.geomToler + grp = list() + processLast = False + + def keyFirst(tup): + return tup[0] + + # Sort edgeData data and prepare proxy indexes + self.edgeData.sort(key=keyFirst) + DATA = self.edgeData + lenDATA = len(DATA) + indexes = [i for i in range(0, lenDATA)] + idxCnt = len(indexes) + + while idxCnt > 0: + processLast = True + # Pop off index for first edge + actvIdx = indexes.pop(0) + actvItem = DATA[actvIdx][0] # 0 index is length + grp.append(actvIdx) + idxCnt -= 1 + noMatch = True + + while idxCnt > 0: + tstIdx = indexes[0] + tstItem = DATA[tstIdx][0] + + # test case(s) goes here + absLenDiff = abs(tstItem - actvItem) + if absLenDiff < threshold: + # Remove test index from indexes + indexes.pop(0) + idxCnt -= 1 + grp.append(tstIdx) + noMatch = False + else: + if len(grp) > 1: + # grp.sort() + self.idGroups.append(grp) + grp = list() + break + # Ewhile + # Ewhile + if processLast: + if len(grp) > 1: + # grp.sort() + self.idGroups.append(grp) + + def _identifySharedEdgesByLength(self, grp): + holds = list() + cont = True + specialIndexes = [] + threshold = self.geomToler + + def keyFirst(tup): + return tup[0] + + # Sort edgeData data + self.edgeData.sort(key=keyFirst) + DATA = self.edgeData + lenDATA = len(DATA) + lenGrp = len(grp) + + while lenGrp > 0: + # Pop off index for first edge + actvIdx = grp.pop(0) + actvItem = DATA[actvIdx][0] # 0 index is length + lenGrp -= 1 + while lenGrp > 0: + isTrue = False + # Pop off index for test edge + tstIdx = grp.pop(0) + tstItem = DATA[tstIdx][0] + lenGrp -= 1 + + # test case(s) goes here + lenDiff = tstItem - actvItem + absLenDiff = abs(lenDiff) + if lenDiff > threshold: + break + if absLenDiff < threshold: + com1 = DATA[actvIdx][1] + com2 = DATA[tstIdx][1] + comDiff = com2.sub(com1).Length + if comDiff < threshold: + isTrue = True + + # Action if test is true (finds special case) + if isTrue: + specialIndexes.append(actvIdx) + specialIndexes.append(tstIdx) + break + else: + holds.append(tstIdx) + + # Put hold indexes back in search group + holds.extend(grp) + grp = holds + lenGrp = len(grp) + holds = list() + + if len(specialIndexes) > 0: + # Remove shared edges from EDGES data + uniqueShared = list(set(specialIndexes)) + self.sharedEdgeIdxs.extend(uniqueShared) + self.noSharedEdges = False + + def _extractWiresFromEdges(self): + DATA = self.edgeData + holds = list() + lastEdge = None + lastIdx = None + firstEdge = None + isWire = False + cont = True + connectedEdges = [] + connectedIndexes = [] + connectedCnt = 0 + LOOPS = list() + + def faceIndex(tup): + return tup[3] + + def faceArea(face): + return face.Area + + # Sort by face index on original model base + DATA.sort(key=faceIndex) + lenDATA = len(DATA) + indexes = [i for i in range(0, lenDATA)] + idxCnt = len(indexes) + + # Add circle edges into REGIONS list + if len(self.circleData) > 0: + for C in self.circleData: + face = Part.Face(Part.Wire(C[2])) + self.REGIONS.append(face) + + actvIdx = indexes.pop(0) + actvEdge = DATA[actvIdx][2] + firstEdge = actvEdge # DATA[connectedIndexes[0]][2] + idxCnt -= 1 + connectedIndexes.append(actvIdx) + connectedEdges.append(actvEdge) + connectedCnt = 1 + + safety = 750 + while cont: # safety > 0 + safety -= 1 + notConnected = True + while idxCnt > 0: + isTrue = False + # Pop off index for test edge + tstIdx = indexes.pop(0) + tstEdge = DATA[tstIdx][2] + idxCnt -= 1 + if self._edgesAreConnected(actvEdge, tstEdge): + isTrue = True + + if isTrue: + notConnected = False + connectedIndexes.append(tstIdx) + connectedEdges.append(tstEdge) + connectedCnt += 1 + actvIdx = tstIdx + actvEdge = tstEdge + break + else: + holds.append(tstIdx) + # Ewhile + + if connectedCnt > 2: + if self._edgesAreConnected(actvEdge, firstEdge): + notConnected = False + # Save loop components + LOOPS.append(connectedEdges) + # reset connected variables and re-assess + connectedEdges = [] + connectedIndexes = [] + connectedCnt = 0 + indexes.sort() + idxCnt = len(indexes) + if idxCnt > 0: + # Pop off index for first edge + actvIdx = indexes.pop(0) + actvEdge = DATA[actvIdx][2] + idxCnt -= 1 + firstEdge = actvEdge + connectedIndexes.append(actvIdx) + connectedEdges.append(actvEdge) + connectedCnt = 1 + # Eif + + # Put holds indexes back in search stack + if notConnected: + holds.append(actvIdx) + if idxCnt == 0: + lastLoop = True + holds.extend(indexes) + indexes = holds + idxCnt = len(indexes) + holds = list() + if idxCnt == 0: + cont = False + # Ewhile + + if len(LOOPS) > 0: + FACES = list() + for Edges in LOOPS: + wire = Part.Wire(Part.__sortEdges__(Edges)) + if wire.isClosed(): + face = Part.Face(wire) + self.REGIONS.append(face) + self.REGIONS.sort(key=faceArea, reverse=True) + + def _identifyInternalFeatures(self): + remList = list() + + for (top, fcIdx) in self.topFaces: + big = Part.Face(top.OuterWire) + for s in range(0, len(self.REGIONS)): + if s not in remList: + small = self.REGIONS[s] + if self._isInBoundBox(big, small): + cmn = big.common(small) + if cmn.Area > 0.0: + self.INTERNALS.append(small) + remList.append(s) + break + else: + FreeCAD.Console.PrintWarning(' - No common area.\n') + + remList.sort(reverse=True) + for ri in remList: + self.REGIONS.pop(ri) + + def _processNestedRegions(self): + cont = True + hold = list() + Ids = list() + remList = list() + for i in range(0, len(self.REGIONS)): + Ids.append(i) + idsCnt = len(Ids) + + while cont: + while idsCnt > 0: + hi = Ids.pop(0) + high = self.REGIONS[hi] + idsCnt -= 1 + while idsCnt > 0: + isTrue = False + li = Ids.pop(0) + idsCnt -= 1 + low = self.REGIONS[li] + # Test case here + if self._isInBoundBox(high, low): + cmn = high.common(low) + if cmn.Area > 0.0: + isTrue = True + # if True action here + if isTrue: + self.REGIONS[hi] = high.cut(low) + # self.INTERNALS.append(low) + remList.append(li) + else: + hold.append(hi) + # Ewhile + hold.extend(Ids) + Ids = hold + hold = list() + if len(Ids) == 0: + cont = False + # Ewhile + # Ewhile + remList.sort(reverse=True) + for ri in remList: + self.REGIONS.pop(ri) + + # Accessory methods + def _getCompleteCrossSection(self, shape): + PathLog.debug('_getCompleteCrossSection()') + wires = list() + bb = shape.BoundBox + mid = (bb.ZMin + bb.ZMax) / 2.0 + + for i in shape.slice(FreeCAD.Vector(0, 0, 1), mid): + wires.append(i) + + if len(wires) > 0: + comp = Part.Compound(wires) # produces correct cross-section wire ! + CS = Part.Face(comp.Wires[0]) + CS.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - CS.BoundBox.ZMin)) + return CS + + PathLog.debug(' -No wires from .slice() method') + return False + + def _edgesAreConnected(self, e1, e2): + # Assumes edges are flat and are at Z=0.0 + + def isSameVertex(v1, v2): + # Assumes vertexes at Z=0.0 + if abs(v1.X - v2.X) < 0.000001: + if abs(v1.Y - v2.Y) < 0.000001: + return True + return False + + if isSameVertex(e1.Vertexes[0], e2.Vertexes[0]): + return True + if isSameVertex(e1.Vertexes[0], e2.Vertexes[1]): + return True + if isSameVertex(e1.Vertexes[1], e2.Vertexes[0]): + return True + if isSameVertex(e1.Vertexes[1], e2.Vertexes[1]): + return True + + return False + + def _isInBoundBox(self, outShp, inShp): + obb = outShp.BoundBox + ibb = inShp.BoundBox + + if obb.XMin < ibb.XMin: + if obb.XMax > ibb.XMax: + if obb.YMin < ibb.YMin: + if obb.YMax > ibb.YMax: + return True + return False + + # Public methods + def setTempGroup(self, grpObj): + '''setTempGroup(grpObj)... For debugging, pass temporary object group.''' + self.tempGroup = grpObj + + def getUnifiedRegions(self): + '''getUnifiedRegions()... Returns a list of unified regions from list + of tuples (faceShape, faceIndex) received at instantiation of the class object.''' + self.INTERNALS = list() + if len(self.FACES) == 0: + FreeCAD.Console.PrintError('No (faceShp, faceIdx) tuples received at instantiation of class.') + return [] + + self._extractTopFaces() + lenFaces = len(self.topFaces) + if lenFaces == 0: + return [] + + # if single topFace, return it + if lenFaces == 1: + topFace = self.topFaces[0][0] + # self._showShape(topFace, 'TopFace') + # prepare inner wires as faces for internal features + lenWrs = len(topFace.Wires) + if lenWrs > 1: + for w in range(1, lenWrs): + self.INTERNALS.append(Part.Face(topFace.Wires[w])) + # prepare outer wire as face for return value in list + if hasattr(topFace, 'OuterWire'): + ow = topFace.OuterWire + else: + ow = topFace.Wires[0] + face = Part.Face(ow) + return [face] + + # process multiple top faces, unifying if possible + self._fuseTopFaces() + # for F in self.fusedFaces.Faces: + # self._showShape(F, 'TopFaceFused') + + self._getEdgesData() + self._groupEdgesByLength() + for grp in self.idGroups: + self._identifySharedEdgesByLength(grp) + + if self.noSharedEdges: + PathLog.debug('No shared edges by length detected.\n') + return [topFace for (topFace, fcIdx) in self.topFaces] + else: + # Delete shared edges from edgeData list + # FreeCAD.Console.PrintWarning('self.sharedEdgeIdxs: {}\n'.format(self.sharedEdgeIdxs)) + self.sharedEdgeIdxs.sort(reverse=True) + for se in self.sharedEdgeIdxs: + # seShp = self.edgeData[se][2] + # self._showShape(seShp, 'SharedEdge') + self.edgeData.pop(se) + + self._extractWiresFromEdges() + self._identifyInternalFeatures() + self._processNestedRegions() + for ri in range(0, len(self.REGIONS)): + self._showShape(self.REGIONS[ri], 'UnifiedRegion_{}'.format(ri)) + + return self.REGIONS + + def getInternalFeatures(self): + '''getInternalFeatures()... Returns internal features identified + after calling getUnifiedRegions().''' + if self.INTERNALS: + return self.INTERNALS + FreeCAD.Console.PrintError('getUnifiedRegions() must be called before getInternalFeatures().\n') + return False +# Eclass \ No newline at end of file diff --git a/src/Mod/Path/PathScripts/PathWaterline.py b/src/Mod/Path/PathScripts/PathWaterline.py index 1bf1c250df..e23b0202fc 100644 --- a/src/Mod/Path/PathScripts/PathWaterline.py +++ b/src/Mod/Path/PathScripts/PathWaterline.py @@ -54,7 +54,6 @@ import math # lazily loaded modules from lazy_loader.lazy_loader import LazyLoader -MeshPart = LazyLoader('MeshPart', globals(), 'MeshPart') Part = LazyLoader('Part', globals(), 'Part') if FreeCAD.GuiUp: @@ -72,19 +71,18 @@ def translate(context, text, disambig=None): class ObjectWaterline(PathOp.ObjectOp): '''Proxy object for Surfacing operation.''' - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - def opFeatures(self, obj): - '''opFeatures(obj) ... return all standard features and edges based geomtries''' - return PathOp.FeatureTool | PathOp.FeatureDepths | PathOp.FeatureHeights | PathOp.FeatureStepDown | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces + '''opFeatures(obj) ... return all standard features''' + return PathOp.FeatureTool | PathOp.FeatureDepths \ + | PathOp.FeatureHeights | PathOp.FeatureStepDown \ + | PathOp.FeatureCoolant | PathOp.FeatureBaseFaces def initOperation(self, obj): - '''initPocketOp(obj) ... - Initialize the operation - property creation and property editor status.''' - self.initOpProperties(obj) + '''initOperation(obj) ... Initialize the operation by + managing property creation and property editor status.''' + self.propertiesReady = False + + self.initOpProperties(obj) # Initialize operation-specific properties # For debugging if PathLog.getLevel(PathLog.thisModule()) != 4: @@ -95,28 +93,30 @@ class ObjectWaterline(PathOp.ObjectOp): def initOpProperties(self, obj, warn=False): '''initOpProperties(obj) ... create operation specific properties''' - missing = list() + self.addNewProps = list() - for (prtyp, nm, grp, tt) in self.opProperties(): + for (prtyp, nm, grp, tt) in self.opPropertyDefinitions(): if not hasattr(obj, nm): obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - if warn: - newPropMsg = translate('PathWaterline', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' - newPropMsg += translate('PathWaterline', 'Check its default value.') - PathLog.warning(newPropMsg) + self.addNewProps.append(nm) # Set enumeration lists for enumeration properties - if len(missing) > 0: - ENUMS = self.propertyEnumerations() + if len(self.addNewProps) > 0: + ENUMS = self.opPropertyEnumerations() for n in ENUMS: - if n in missing: + if n in self.addNewProps: setattr(obj, n, ENUMS[n]) - self.addedAllProperties = True + if warn: + newPropMsg = translate('PathWaterline', 'New property added to') + newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + '. ' + newPropMsg += translate('PathWaterline', 'Check default value(s).') + FreeCAD.Console.PrintWarning(newPropMsg + '\n') - def opProperties(self): - '''opProperties() ... return list of tuples containing operation specific properties''' + self.propertiesReady = True + + def opPropertyDefinitions(self): + '''opPropertyDefinitions() ... return list of tuples containing operation specific properties''' return [ ("App::PropertyBool", "ShowTempObjects", "Debug", QtCore.QT_TRANSLATE_NOOP("App::Property", "Show the temporary path construction objects when module is in DEBUG mode.")), @@ -167,7 +167,7 @@ class ObjectWaterline(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Choose location of the center point for starting the cut pattern.")), ("App::PropertyDistance", "SampleInterval", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the sampling resolution. Smaller values quickly increase processing time.")), - ("App::PropertyPercent", "StepOver", "Clearing Options", + ("App::PropertyFloat", "StepOver", "Clearing Options", QtCore.QT_TRANSLATE_NOOP("App::Property", "Set the stepover percentage, based on the tool's diameter.")), ("App::PropertyBool", "OptimizeLinearPaths", "Optimization", @@ -185,7 +185,7 @@ class ObjectWaterline(PathOp.ObjectOp): QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if specifying a Start Point")) ] - def propertyEnumerations(self): + def opPropertyEnumerations(self): # Enumeration lists for App::PropertyEnumeration properties return { 'Algorithm': ['OCL Dropcutter', 'Experimental'], @@ -198,6 +198,56 @@ class ObjectWaterline(PathOp.ObjectOp): 'LayerMode': ['Single-pass', 'Multi-pass'], } + def opPropertyDefaults(self, obj, job): + '''opPropertyDefaults(obj, job) ... returns a dictionary + of default values for the operation's properties.''' + defaults = { + 'OptimizeLinearPaths': True, + 'InternalFeaturesCut': True, + 'OptimizeStepOverTransitions': False, + 'BoundaryEnforcement': True, + 'UseStartPoint': False, + 'AvoidLastX_InternalFeatures': True, + 'CutPatternReversed': False, + 'IgnoreOuterAbove': obj.StartDepth.Value + 0.00001, + 'StartPoint': FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value), + 'Algorithm': 'OCL Dropcutter', + 'LayerMode': 'Single-pass', + 'CutMode': 'Conventional', + 'CutPattern': 'None', + 'HandleMultipleFeatures': 'Collectively', + 'PatternCenterAt': 'CenterOfMass', + 'GapSizes': 'No gaps identified.', + 'ClearLastLayer': 'Off', + 'StepOver': 100.0, + 'CutPatternAngle': 0.0, + 'DepthOffset': 0.0, + 'SampleInterval': 1.0, + 'BoundaryAdjustment': 0.0, + 'InternalFeaturesAdjustment': 0.0, + 'AvoidLastX_Faces': 0, + 'PatternCenterCustom': FreeCAD.Vector(0.0, 0.0, 0.0), + 'GapThreshold': 0.005, + 'AngularDeflection': 0.25, + 'LinearDeflection': 0.0001, + # For debugging + 'ShowTempObjects': False + } + + warn = True + if hasattr(job, 'GeometryTolerance'): + if job.GeometryTolerance.Value != 0.0: + warn = False + defaults['LinearDeflection'] = job.GeometryTolerance.Value + if warn: + msg = translate('PathWaterline', + 'The GeometryTolerance for this Job is 0.0.') + msg += translate('PathWaterline', + 'Initializing LinearDeflection to 0.0001 mm.') + FreeCAD.Console.PrintWarning(msg + '\n') + + return defaults + def setEditorProperties(self, obj): # Used to hide inputs in properties list expMode = G = 0 @@ -251,21 +301,23 @@ class ObjectWaterline(PathOp.ObjectOp): obj.setEditorMode('AngularDeflection', expMode) def onChanged(self, obj, prop): - if hasattr(self, 'addedAllProperties'): - if self.addedAllProperties is True: + if hasattr(self, 'propertiesReady'): + if self.propertiesReady: if prop in ['Algorithm', 'CutPattern']: self.setEditorProperties(obj) def opOnDocumentRestored(self, obj): - self.initOpProperties(obj, warn=True) + self.propertiesReady = False + job = PathUtils.findParentJob(obj) - if PathLog.getLevel(PathLog.thisModule()) != 4: - obj.setEditorMode('ShowTempObjects', 2) # hide - else: - obj.setEditorMode('ShowTempObjects', 0) # show + self.initOpProperties(obj, warn=True) + self.opApplyPropertyDefaults(obj, job, self.addNewProps) + + mode = 2 if PathLog.getLevel(PathLog.thisModule()) != 4 else 0 + obj.setEditorMode('ShowTempObjects', mode) # Repopulate enumerations in case of changes - ENUMS = self.propertyEnumerations() + ENUMS = self.opPropertyEnumerations() for n in ENUMS: restore = False if hasattr(obj, n): @@ -277,40 +329,28 @@ class ObjectWaterline(PathOp.ObjectOp): self.setEditorProperties(obj) + def opApplyPropertyDefaults(self, obj, job, propList): + # Set standard property defaults + PROP_DFLTS = self.opPropertyDefaults(obj, job) + for n in PROP_DFLTS: + if n in propList: + prop = getattr(obj, n) + val = PROP_DFLTS[n] + setVal = False + if hasattr(prop, 'Value'): + if isinstance(val, int) or isinstance(val, float): + setVal = True + if setVal: + propVal = getattr(prop, 'Value') + setattr(prop, 'Value', val) + else: + setattr(obj, n, val) + def opSetDefaultValues(self, obj, job): '''opSetDefaultValues(obj, job) ... initialize defaults''' job = PathUtils.findParentJob(obj) - obj.OptimizeLinearPaths = True - obj.InternalFeaturesCut = True - obj.OptimizeStepOverTransitions = False - obj.BoundaryEnforcement = True - obj.UseStartPoint = False - obj.AvoidLastX_InternalFeatures = True - obj.CutPatternReversed = False - obj.IgnoreOuterAbove = obj.StartDepth.Value + 0.00001 - obj.StartPoint = FreeCAD.Vector(0.0, 0.0, obj.ClearanceHeight.Value) - obj.Algorithm = 'OCL Dropcutter' - obj.LayerMode = 'Single-pass' - obj.CutMode = 'Conventional' - obj.CutPattern = 'None' - obj.HandleMultipleFeatures = 'Collectively' # 'Individually' - obj.PatternCenterAt = 'CenterOfMass' # 'CenterOfBoundBox', 'XminYmin', 'Custom' - obj.GapSizes = 'No gaps identified.' - obj.ClearLastLayer = 'Off' - obj.StepOver = 100 - obj.CutPatternAngle = 0.0 - obj.DepthOffset.Value = 0.0 - obj.SampleInterval.Value = 1.0 - obj.BoundaryAdjustment.Value = 0.0 - obj.InternalFeaturesAdjustment.Value = 0.0 - obj.AvoidLastX_Faces = 0 - obj.PatternCenterCustom = FreeCAD.Vector(0.0, 0.0, 0.0) - obj.GapThreshold.Value = 0.005 - obj.LinearDeflection.Value = 0.0001 - obj.AngularDeflection.Value = 0.25 - # For debugging - obj.ShowTempObjects = False + self.opApplyPropertyDefaults(obj, job, self.addNewProps) # need to overwrite the default depth calculations for facing d = None @@ -353,10 +393,10 @@ class ObjectWaterline(PathOp.ObjectOp): PathLog.error(translate('PathWaterline', 'Cut pattern angle limits are +- 360 degrees.')) # Limit StepOver to natural number percentage - if obj.StepOver > 100: - obj.StepOver = 100 - if obj.StepOver < 1: - obj.StepOver = 1 + if obj.StepOver > 100.0: + obj.StepOver = 100.0 + if obj.StepOver < 1.0: + obj.StepOver = 1.0 # Limit AvoidLastX_Faces to zero and positive values if obj.AvoidLastX_Faces < 0: @@ -536,13 +576,12 @@ class ObjectWaterline(PathOp.ObjectOp): self.modelSTLs = PSF.modelSTLs self.profileShapes = PSF.profileShapes - # Create OCL.stl model objects - if obj.Algorithm == 'OCL Dropcutter': - self._prepareModelSTLs(JOB, obj) - PathLog.debug('obj.LinearDeflection.Value: {}'.format(obj.LinearDeflection.Value)) - PathLog.debug('obj.AngularDeflection.Value: {}'.format(obj.AngularDeflection.Value)) for m in range(0, len(JOB.Model.Group)): + # Create OCL.stl model objects + if obj.Algorithm == 'OCL Dropcutter': + PathSurfaceSupport._prepareModelSTLs(self, JOB, obj, m, ocl) + Mdl = JOB.Model.Group[m] if FACES[m] is False: PathLog.error('No data for model base: {}'.format(JOB.Model.Group[m].Label)) @@ -554,7 +593,7 @@ class ObjectWaterline(PathOp.ObjectOp): PathLog.info('Working on Model.Group[{}]: {}'.format(m, Mdl.Label)) # make stock-model-voidShapes STL model for avoidance detection on transitions if obj.Algorithm == 'OCL Dropcutter': - self._makeSafeSTL(JOB, obj, m, FACES[m], VOIDS[m]) + PathSurfaceSupport._makeSafeSTL(self, JOB, obj, m, FACES[m], VOIDS[m], ocl) # Process model/faces - OCL objects must be ready CMDS.extend(self._processWaterlineAreas(JOB, obj, m, FACES[m], VOIDS[m])) @@ -627,114 +666,7 @@ class ObjectWaterline(PathOp.ObjectOp): return True - # Methods for constructing the cut area - def _prepareModelSTLs(self, JOB, obj): - PathLog.debug('_prepareModelSTLs()') - for m in range(0, len(JOB.Model.Group)): - M = JOB.Model.Group[m] - - # PathLog.debug(f" -self.modelTypes[{m}] == 'M'") - if self.modelTypes[m] == 'M': - # TODO: test if this works - facets = M.Mesh.Facets.Points - else: - facets = Part.getFacets(M.Shape) - - if self.modelSTLs[m] is True: - stl = ocl.STLSurf() - - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - self.modelSTLs[m] = stl - return - - def _makeSafeSTL(self, JOB, obj, mdlIdx, faceShapes, voidShapes): - '''_makeSafeSTL(JOB, obj, mdlIdx, faceShapes, voidShapes)... - Creates and OCL.stl object with combined data with waste stock, - model, and avoided faces. Travel lines can be checked against this - STL object to determine minimum travel height to clear stock and model.''' - PathLog.debug('_makeSafeSTL()') - - fuseShapes = list() - Mdl = JOB.Model.Group[mdlIdx] - mBB = Mdl.Shape.BoundBox - sBB = JOB.Stock.Shape.BoundBox - - # add Model shape to safeSTL shape - fuseShapes.append(Mdl.Shape) - - if obj.BoundBox == 'BaseBoundBox': - cont = False - extFwd = (sBB.ZLength) - zmin = mBB.ZMin - zmax = mBB.ZMin + extFwd - stpDwn = (zmax - zmin) / 4.0 - dep_par = PathUtils.depth_params(zmax + 5.0, zmax + 3.0, zmax, stpDwn, 0.0, zmin) - - try: - envBB = PathUtils.getEnvelope(partshape=Mdl.Shape, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as ee: - PathLog.error(str(ee)) - shell = Mdl.Shape.Shells[0] - solid = Part.makeSolid(shell) - try: - envBB = PathUtils.getEnvelope(partshape=solid, depthparams=dep_par) # Produces .Shape - cont = True - except Exception as eee: - PathLog.error(str(eee)) - - if cont: - stckWst = JOB.Stock.Shape.cut(envBB) - if obj.BoundaryAdjustment > 0.0: - cmpndFS = Part.makeCompound(faceShapes) - baBB = PathUtils.getEnvelope(partshape=cmpndFS, depthparams=self.depthParams) # Produces .Shape - adjStckWst = stckWst.cut(baBB) - else: - adjStckWst = stckWst - fuseShapes.append(adjStckWst) - else: - PathLog.warning('Path transitions might not avoid the model. Verify paths.') - else: - # If boundbox is Job.Stock, add hidden pad under stock as base plate - toolDiam = self.cutter.getDiameter() - zMin = JOB.Stock.Shape.BoundBox.ZMin - xMin = JOB.Stock.Shape.BoundBox.XMin - toolDiam - yMin = JOB.Stock.Shape.BoundBox.YMin - toolDiam - bL = JOB.Stock.Shape.BoundBox.XLength + (2 * toolDiam) - bW = JOB.Stock.Shape.BoundBox.YLength + (2 * toolDiam) - bH = 1.0 - crnr = FreeCAD.Vector(xMin, yMin, zMin - 1.0) - B = Part.makeBox(bL, bW, bH, crnr, FreeCAD.Vector(0, 0, 1)) - fuseShapes.append(B) - - if voidShapes is not False: - voidComp = Part.makeCompound(voidShapes) - voidEnv = PathUtils.getEnvelope(partshape=voidComp, depthparams=self.depthParams) # Produces .Shape - fuseShapes.append(voidEnv) - - fused = Part.makeCompound(fuseShapes) - - if self.showDebugObjects is True: - T = FreeCAD.ActiveDocument.addObject('Part::Feature', 'safeSTLShape') - T.Shape = fused - T.purgeTouched() - self.tempGroup.addObject(T) - - facets = Part.getFacets(fused) - - stl = ocl.STLSurf() - for tri in facets: - t = ocl.Triangle(ocl.Point(tri[0][0], tri[0][1], tri[0][2]), - ocl.Point(tri[1][0], tri[1][1], tri[1][2]), - ocl.Point(tri[2][0], tri[2][1], tri[2][2])) - stl.addTriangle(t) - - self.safeSTLs[mdlIdx] = stl - + # Methods for constructing the cut area and creating path geometry def _processWaterlineAreas(self, JOB, obj, mdlIdx, FCS, VDS): '''_processWaterlineAreas(JOB, obj, mdlIdx, FCS, VDS)... This method applies any avoided faces or regions to the selected faces. @@ -787,7 +719,6 @@ class ObjectWaterline(PathOp.ObjectOp): return final - # Methods for creating path geometry def _getExperimentalWaterlinePaths(self, PNTSET, csHght, cutPattern): '''_getExperimentalWaterlinePaths(PNTSET, csHght, cutPattern)... Switching function for calling the appropriate path-geometry to OCL points conversion function @@ -864,7 +795,6 @@ class ObjectWaterline(PathOp.ObjectOp): height = first.z elif (minSTH + (2.0 * tolrnc)) >= max(first.z, lstPnt.z): height = False # allow end of Zig to cut to beginning of Zag - # Create raise, shift, and optional lower commands if height is not False: @@ -979,7 +909,9 @@ class ObjectWaterline(PathOp.ObjectOp): scanLines[L].append(oclScan[pi]) lenSL = len(scanLines) pntsPerLine = len(scanLines[0]) - PathLog.debug("--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line") + msg = "--OCL scan: " + str(lenSL * pntsPerLine) + " points, with " + msg += str(numScanLines) + " lines and " + str(pntsPerLine) + " pts/line" + PathLog.debug(msg) # Extract Wl layers per depthparams lyr = 0 @@ -1694,7 +1626,7 @@ class ObjectWaterline(PathOp.ObjectOp): return False def _getModelCrossSection(self, shape, csHght): - PathLog.debug('getCrossSection()') + PathLog.debug('_getModelCrossSection()') wires = list() def byArea(fc): diff --git a/src/Mod/Path/PathScripts/PathWaterlineGui.py b/src/Mod/Path/PathScripts/PathWaterlineGui.py index 0616bbe6d2..ad4e06ba93 100644 --- a/src/Mod/Path/PathScripts/PathWaterlineGui.py +++ b/src/Mod/Path/PathScripts/PathWaterlineGui.py @@ -107,8 +107,8 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): return signals - def updateVisibility(self): - '''updateVisibility()... Updates visibility of Tasks panel objects.''' + def updateVisibility(self, sentObj=None): + '''updateVisibility(sentObj=None)... Updates visibility of Tasks panel objects.''' Algorithm = self.form.algorithmSelect.currentText() self.form.optimizeEnabled.hide() # Has no independent QLabel object