def adjustWirePlacement(obj, shape, wires): job = PathUtils.findParentJob(obj) if hasattr(shape, 'MapMode') and 'Deactivated' != shape.MapMode: if hasattr(shape, 'Support') and 1 == len(shape.Support) and 1 == len(shape.Support[0][1]): pmntShape = shape.Placement pmntSupport = shape.Support[0][0].getGlobalPlacement() #pmntSupport = shape.Support[0][0].Placement pmntBase = job.Base.Placement pmnt = pmntBase.multiply(pmntSupport.inverse().multiply(pmntShape)) #PathLog.debug("pmnt = %s" % pmnt) newWires = [] for w in wires: edges = [] for e in w.Edges: e = e.copy() e.Placement = FreeCAD.Placement() edges.append(e) w = Part.Wire(edges) w.Placement = pmnt newWires.append(w) wires = newWires else: PathLog.warning(translate("PathEngrave", "Attachment not supported by engraver")) else: PathLog.debug("MapMode: %s" % (shape.MapMode if hasattr(shape, 'MapMode') else 'None')) return wires
def areaOpShapeForDepths(self, obj): '''areaOpShapeForDepths(obj) ... returns the shape used to make an initial calculation for the depths being used. The default implementation returns the job's Base.Shape''' job = PathUtils.findParentJob(obj) if job and job.Base: PathLog.debug("job=%s base=%s shape=%s" % (job, job.Base, job.Base.Shape)) return job.Base.Shape if job: PathLog.warning(translate("PathAreaOp", "job %s has no Base.") % job.Label) else: PathLog.warning(translate("PathAreaOp", "no job for op %s found.") % obj.Label) return None
def __init__(self, widget, obj, prop, onBeforeChange=None): self.obj = obj self.widget = widget self.prop = prop self.onBeforeChange = onBeforeChange attr = getProperty(self.obj, self.prop) if attr is not None: if hasattr(attr, 'Value'): widget.setProperty('unit', attr.getUserPreferred()[2]) widget.setProperty('binding', "%s.%s" % (obj.Name, prop)) self.valid = True else: PathLog.warning(translate('PathGui', "Cannot find property %s of %s") % (prop, obj.Label)) self.valid = False
def _getProperty(obj, prop): o = obj attr = obj for name in prop.split('.'): o = attr if not hasattr(o, name): break attr = getattr(o, name) if o == attr: PathLog.warning(translate('PathGui', "%s has no property %s (%s))") % (obj.Label, prop, name)) return (None, None, None) #PathLog.debug("found property %s of %s (%s: %s)" % (prop, obj.Label, name, attr)) return(o, attr, name)
def flipEdge(edge): '''flipEdge(edge) Flips given edge around so the new Vertexes[0] was the old Vertexes[-1] and vice versa, without changing the shape. Currently only lines, line segments, circles and arcs are supported.''' if Part.Line == type(edge.Curve) and not edge.Vertexes: return Part.Edge(Part.Line(edge.valueAt(edge.LastParameter), edge.valueAt(edge.FirstParameter))) elif Part.Line == type(edge.Curve) or Part.LineSegment == type(edge.Curve): return Part.Edge(Part.LineSegment(edge.Vertexes[-1].Point, edge.Vertexes[0].Point)) elif Part.Circle == type(edge.Curve): # Create an inverted circle circle = Part.Circle(edge.Curve.Center, -edge.Curve.Axis, edge.Curve.Radius) # Rotate the circle appropriately so it starts at edge.valueAt(edge.LastParameter) circle.rotate(FreeCAD.Placement(circle.Center, circle.Axis, 180 - math.degrees(edge.LastParameter + edge.Curve.AngleXU))) # Now the edge always starts at 0 and LastParameter is the value range arc = Part.Edge(circle, 0, edge.LastParameter - edge.FirstParameter) return arc elif Part.BSplineCurve == type(edge.Curve): spline = edge.Curve mults = spline.getMultiplicities() weights = spline.getWeights() knots = spline.getKnots() poles = spline.getPoles() perio = spline.isPeriodic() ratio = spline.isRational() degree = spline.Degree ma = max(knots) mi = min(knots) knots = [ma+mi-k for k in knots] mults.reverse() weights.reverse() poles.reverse() knots.reverse() flipped = Part.BSplineCurve() flipped.buildFromPolesMultsKnots(poles, mults , knots, perio, degree, weights, ratio) return Part.Edge(flipped) global OddsAndEnds OddsAndEnds.append(edge) PathLog.warning(translate('PathGeom', "%s not support for flipping") % type(edge.Curve))
def opExecute(self, obj): '''opExecute(obj) ... processes all Base features and Locations and collects them in a list of positions and radii which is then passed to circularHoleExecute(obj, holes). If no Base geometries and no Locations are present, the job's Base is inspected and all drillable features are added to Base. In this case appropriate values for depths are also calculated and assigned. Do not overwrite, implement circularHoleExecute(obj, holes) instead.''' PathLog.track() holes = [] baseSubsTuples = [] subCount = 0 allTuples = [] self.cloneNames = [] # pylint: disable=attribute-defined-outside-init self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init self.rotateFlag = False # pylint: disable=attribute-defined-outside-init self.useTempJobClones('Delete') # pylint: disable=attribute-defined-outside-init self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init self.clearHeight = obj.ClearanceHeight.Value # pylint: disable=attribute-defined-outside-init self.safeHeight = obj.SafeHeight.Value # pylint: disable=attribute-defined-outside-init self.axialFeed = 0.0 # pylint: disable=attribute-defined-outside-init self.axialRapid = 0.0 # pylint: disable=attribute-defined-outside-init trgtDep = None def haveLocations(self, obj): if PathOp.FeatureLocations & self.opFeatures(obj): return len(obj.Locations) != 0 return False if obj.EnableRotation == 'Off': strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value else: # Calculate operation heights based upon rotation radii opHeights = self.opDetermineRotationRadii(obj) (self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init (clrOfset, safOfst) = opHeights[1] PathLog.debug("Exec. opHeights[0]: " + str(opHeights[0])) PathLog.debug("Exec. opHeights[1]: " + str(opHeights[1])) # Set clearance and safe heights based upon rotation radii if obj.EnableRotation == 'A(x)': strDep = self.xRotRad elif obj.EnableRotation == 'B(y)': strDep = self.yRotRad else: strDep = max(self.xRotRad, self.yRotRad) finDep = -1 * strDep obj.ClearanceHeight.Value = strDep + clrOfset obj.SafeHeight.Value = strDep + safOfst # Create visual axes when debugging. if PathLog.getLevel(PathLog.thisModule()) == 4: self.visualAxis() # Set axial feed rates based upon horizontal feed rates safeCircum = 2 * math.pi * obj.SafeHeight.Value self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init # Complete rotational analysis and temp clone creation as needed if obj.EnableRotation == 'Off': PathLog.debug("Enable Rotation setting is 'Off' for {}.".format( obj.Name)) stock = PathUtils.findParentJob(obj).Stock for (base, subList) in obj.Base: baseSubsTuples.append((base, subList, 0.0, 'A', stock)) else: for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] for sub in subsList: if self.isHoleEnabled(obj, base, sub): shape = getattr(base.Shape, sub) rtn = False (norm, surf) = self.getFaceNormAndSurf(shape) (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable if rtn is True: (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis( obj, base, angle, axis, subCount) # Verify faces are correctly oriented - InverseAngle might be necessary PathLog.debug( "Verifying {} orientation: running faceRotationAnalysis() again." .format(sub)) faceIA = getattr(clnBase.Shape, sub) (norm, surf) = self.getFaceNormAndSurf(faceIA) (rtn, praAngle, praAxis, praInfo) = self.faceRotationAnalysis( obj, norm, surf) # pylint: disable=unused-variable if rtn is True: msg = obj.Name + ":: " msg += translate( "Path", "{} might be misaligned after initial rotation." .format(sub)) + " " if obj.AttemptInverseAngle is True and obj.InverseAngle is False: (clnBase, clnStock, angle) = self.applyInverseAngle( obj, clnBase, clnStock, axis, angle) msg += translate( "Path", "Rotated to 'InverseAngle' to attempt access." ) else: if len(subsList) == 1: msg += translate( "Path", "Consider toggling the 'InverseAngle' property and recomputing." ) else: msg += translate( "Path", "Consider transferring '{}' to independent operation." .format(sub)) PathLog.warning(msg) # title = translate("Path", 'Rotation Warning') # self.guiMessage(title, msg, False) else: PathLog.debug( "Face appears to be oriented correctly.") cmnt = "{}: {} @ {}; ".format( sub, axis, str(round(angle, 5))) if cmnt not in obj.Comment: obj.Comment += cmnt tup = clnBase, sub, tag, angle, axis, clnStock allTuples.append(tup) else: if self.warnDisabledAxis(obj, axis, sub) is True: pass # Skip drill feature due to access issue else: PathLog.debug(str(sub) + ": No rotation used") axis = 'X' angle = 0.0 tag = base.Name + '_' + axis + str( angle).replace('.', '_') stock = PathUtils.findParentJob(obj).Stock tup = base, sub, tag, angle, axis, stock allTuples.append(tup) # Eif # Eif subCount += 1 # Efor # Efor (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) subList = [] for o in range(0, len(Tags)): PathLog.debug('hTag: {}'.format(Tags[o])) subList = [] for (base, sub, tag, angle, axis, stock) in Grps[o]: subList.append(sub) pair = base, subList, angle, axis, stock baseSubsTuples.append(pair) # Efor for base, subs, angle, axis, stock in baseSubsTuples: for sub in subs: if self.isHoleEnabled(obj, base, sub): pos = self.holePosition(obj, base, sub) if pos: # Default is treat selection as 'Face' shape finDep = base.Shape.getElement(sub).BoundBox.ZMin if base.Shape.getElement(sub).ShapeType == 'Edge': msg = translate( "Path", "Verify Final Depth of holes based on edges. {} depth is: {} mm" .format(sub, round(finDep, 4))) + " " msg += translate( "Path", "Always select the bottom edge of the hole when using an edge." ) PathLog.warning(msg) # If user has not adjusted Final Depth value, attempt to determine from sub trgtDep = obj.FinalDepth.Value if obj.OpFinalDepth.Value == obj.FinalDepth.Value: trgtDep = finDep if obj.FinalDepth.Value < finDep: msg = translate( "Path", "Final Depth setting is below the hole bottom for {}." .format(sub)) PathLog.warning(msg) holes.append({ 'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub), 'angle': angle, 'axis': axis, 'trgtDep': trgtDep, 'stkTop': stock.Shape.BoundBox.ZMax }) if haveLocations(self, obj): for location in obj.Locations: # holes.append({'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'finDep': obj.FinalDepth.Value}) trgtDep = obj.FinalDepth.Value holes.append({ 'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'trgtDep': trgtDep, 'stkTop': PathUtils.findParentJob(obj).stock.Shape.BoundBox.ZMax }) # If all holes based upon edges, set post-operation Final Depth to highest edge height if obj.OpFinalDepth.Value == obj.FinalDepth.Value: if len(holes) == 1: PathLog.info( translate( 'Path', "Single-hole operation. Saving Final Depth determined from hole base." )) obj.FinalDepth.Value = trgtDep if len(holes) > 0: self.circularHoleExecute( obj, holes) # circularHoleExecute() located in PathDrilling.py self.useTempJobClones( 'Delete') # Delete temp job clone group and contents self.guiMessage('title', None, show=True) # Process GUI messages to user PathLog.debug("obj.Name: " + str(obj.Name))
def generateRamps(self, allowBounce=True): edges = self.wire.Edges outedges = [] for edge in edges: israpid = False for redge in self.rapids: if PathGeom.edgesMatch(edge, redge): israpid = True if not israpid: bb = edge.BoundBox p0 = edge.Vertexes[0].Point p1 = edge.Vertexes[1].Point rampangle = self.angle if bb.XLength < 1e-6 and bb.YLength < 1e-6 and bb.ZLength > 0 and p0.z > p1.z: # check if above ignoreAbove parameter - do not generate ramp if it is newEdge, cont = self.checkIgnoreAbove(edge) if newEdge is not None: outedges.append(newEdge) p0.z = self.ignoreAbove if cont: continue plungelen = abs(p0.z - p1.z) projectionlen = plungelen * math.tan( math.radians(rampangle) ) # length of the forthcoming ramp projected to XY plane PathLog.debug( "Found plunge move at X:{} Y:{} From Z:{} to Z{}, length of ramp: {}" .format(p0.x, p0.y, p0.z, p1.z, projectionlen)) if self.method == 'RampMethod3': projectionlen = projectionlen / 2 # next need to determine how many edges in the path after # plunge are needed to cover the length: covered = False coveredlen = 0 rampedges = [] i = edges.index(edge) + 1 while not covered: candidate = edges[i] cp0 = candidate.Vertexes[0].Point cp1 = candidate.Vertexes[1].Point if abs(cp0.z - cp1.z) > 1e-6: # this edge is not parallel to XY plane, not qualified for ramping. break # PathLog.debug("Next edge length {}".format(candidate.Length)) rampedges.append(candidate) coveredlen = coveredlen + candidate.Length if coveredlen > projectionlen: covered = True i = i + 1 if i >= len(edges): break if len(rampedges) == 0: PathLog.debug( "No suitable edges for ramping, plunge will remain as such" ) outedges.append(edge) else: if not covered: if (not allowBounce ) or self.method == 'RampMethod2': l = 0 for redge in rampedges: l = l + redge.Length if self.method == 'RampMethod3': rampangle = math.degrees( math.atan(l / (plungelen / 2))) else: rampangle = math.degrees( math.atan(l / plungelen)) PathLog.warning( "Cannot cover with desired angle, tightening angle to: {}" .format(rampangle)) # PathLog.debug("Doing ramp to edges: {}".format(rampedges)) if self.method == 'RampMethod1': outedges.extend( self.createRampMethod1(rampedges, p0, projectionlen, rampangle)) elif self.method == 'RampMethod2': outedges.extend( self.createRampMethod2(rampedges, p0, projectionlen, rampangle)) else: # if the ramp cannot be covered with Method3, revert to Method1 # because Method1 support going back-and-forth and thus results in same path as Method3 when # length of the ramp is smaller than needed for single ramp. if (not covered) and allowBounce: projectionlen = projectionlen * 2 outedges.extend( self.createRampMethod1( rampedges, p0, projectionlen, rampangle)) else: outedges.extend( self.createRampMethod3( rampedges, p0, projectionlen, rampangle)) else: outedges.append(edge) else: outedges.append(edge) return outedges
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.''' PathLog.track() PathLog.debug("----- areaOpShapes() in PathProfileFaces.py") if obj.UseComp: self.commandlist.append( Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")")) else: self.commandlist.append(Path.Command("(Uncompensated Tool Path)")) shapes = [] self.profileshape = [] finalDepths = [] startDepths = [] faceDepths = [] baseSubsTuples = [] subCount = 0 allTuples = [] if obj.Base: # The user has selected subobjects from the base. Process each. if obj.EnableRotation != 'Off': for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] for sub in subsList: subCount += 1 shape = getattr(base.Shape, sub) if isinstance(shape, Part.Face): rtn = False (norm, surf) = self.getFaceNormAndSurf(shape) (rtn, angle, axis, praInfo) = self.faceRotationAnalysis( obj, norm, surf) if rtn is True: (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis( obj, base, angle, axis, subCount) # Verify faces are correctly oriented - InverseAngle might be necessary faceIA = getattr(clnBase.Shape, sub) (norm, surf) = self.getFaceNormAndSurf(faceIA) (rtn, praAngle, praAxis, praInfo) = self.faceRotationAnalysis( obj, norm, surf) if rtn is True: PathLog.error( translate( "Path", "Face appears misaligned after initial rotation." )) if obj.AttemptInverseAngle is True and obj.InverseAngle is False: (clnBase, clnStock, angle) = self.applyInverseAngle( obj, clnBase, clnStock, axis, angle) else: msg = translate( "Path", "Consider toggling the 'InverseAngle' property and recomputing." ) PathLog.error(msg) # title = translate("Path", 'Rotation Warning') # self.guiMessage(title, msg, False) else: PathLog.debug( "Face appears to be oriented correctly." ) tup = clnBase, sub, tag, angle, axis, clnStock else: if self.warnDisabledAxis(obj, axis) is False: PathLog.debug( str(sub) + ": No rotation used") axis = 'X' angle = 0.0 tag = base.Name + '_' + axis + str( angle).replace('.', '_') stock = PathUtils.findParentJob(obj).Stock tup = base, sub, tag, angle, axis, stock # Eif allTuples.append(tup) # Eif # Efor # Efor if subCount > 1: msg = translate('Path', "Multiple faces in Base Geometry.") + " " msg += translate( 'Path', "Depth settings will be applied to all faces.") PathLog.warning(msg) # title = translate("Path", "Depth Warning") # self.guiMessage(title, msg) (Tags, Grps) = self.sortTuplesByIndex( allTuples, 2) # return (TagList, GroupList) subList = [] for o in range(0, len(Tags)): subList = [] for (base, sub, tag, angle, axis, stock) in Grps[o]: subList.append(sub) pair = base, subList, angle, axis, stock baseSubsTuples.append(pair) # Efor else: PathLog.info( 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)) # for base in obj.Base: finish_step = obj.FinishDepth.Value if hasattr( obj, "FinishDepth") else 0.0 for (base, subsList, angle, axis, stock) in baseSubsTuples: holes = [] faces = [] faceDepths = [] startDepths = [] for sub in subsList: shape = getattr(base.Shape, sub) if isinstance(shape, Part.Face): faces.append(shape) if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face for wire in shape.Wires[1:]: holes.append((base.Shape, wire)) # Add face depth to list faceDepths.append(shape.BoundBox.ZMin) else: ignoreSub = base.Name + '.' + sub msg = translate( 'Path', "Found a selected object which is not a face. Ignoring: {}" .format(ignoreSub)) PathLog.error(msg) FreeCAD.Console.PrintWarning(msg) # Raise FinalDepth to lowest face in list on Inside profile ops finDep = obj.FinalDepth.Value if obj.Side == 'Inside': finDep = min(faceDepths) finalDepths.append(finDep) strDep = obj.StartDepth.Value if strDep > stock.Shape.BoundBox.ZMax: strDep = stock.Shape.BoundBox.ZMax startDepths.append(strDep) # Recalculate depthparams self.depthparams = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=strDep, # obj.StartDepth.Value, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=finDep, # obj.FinalDepth.Value, user_depths=None) 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): PathLog.track() env = PathUtils.getEnvelope( shape, subshape=f, depthparams=self.depthparams) # shapes.append((env, True)) tup = env, True, 'pathProfileFaces', angle, axis, strDep, finDep shapes.append(tup) if len(faces) > 0: profileshape = Part.makeCompound(faces) self.profileshape.append(profileshape) if obj.processPerimeter: PathLog.track() try: env = PathUtils.getEnvelope( base.Shape, subshape=profileshape, depthparams=self.depthparams) except Exception: # PathUtils.getEnvelope() failed to return an object. PathLog.error( translate('Path', 'Unable to create path for face(s).')) else: # shapes.append((env, False)) tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep shapes.append(tup) else: for shape in faces: finalDep = finDep # Recalculate depthparams if obj.Side == 'Inside': if finalDep < shape.BoundBox.ZMin: custDepthparams = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=strDep, # obj.StartDepth.Value, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=shape.BoundBox. ZMin, # obj.FinalDepth.Value, user_depths=None) env = PathUtils.getEnvelope( base.Shape, subshape=shape, depthparams=custDepthparams) finalDep = shape.BoundBox.ZMin else: env = PathUtils.getEnvelope( base.Shape, subshape=shape, depthparams=self.depthparams) else: env = PathUtils.getEnvelope( base.Shape, subshape=shape, depthparams=self.depthparams) tup = env, False, 'pathProfileFaces', angle, axis, strDep, finalDep shapes.append(tup) # Eif # adjust Start/Final Depths as needed # Raise existing Final Depth to level of lowest profile face if obj.Side == 'Inside': finalDepth = min(finalDepths) if obj.FinalDepth.Value < finalDepth: obj.FinalDepth.Value = finalDepth # Lower high Start Depth to top of Stock startDepth = max(startDepths) if obj.StartDepth.Value > startDepth: obj.StartDepth.Value = startDepth else: # Try to build targets from the job base if 1 == len(self.model): if hasattr(self.model[0], "Proxy"): PathLog.info("hasattr() Proxy") if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet if obj.processCircles or obj.processHoles: for shape in self.model[0].Proxy.getHoles( self.model[0], transform=True): for wire in shape.Wires: drillable = PathUtils.isDrillable( self.model[0].Proxy, wire) 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) # shapes.append((env, True)) tup = env, True, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) if obj.processPerimeter: for shape in self.model[0].Proxy.getOutlines( self.model[0], transform=True): for wire in shape.Wires: f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope( self.model[0].Shape, subshape=f, depthparams=self.depthparams) # shapes.append((env, False)) tup = env, False, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) self.removalshapes = shapes PathLog.debug("%d shapes" % len(shapes)) return shapes
def areaOpShapes(self, obj): """areaOpShapes(obj) ... return shapes representing the solids to be removed.""" PathLog.track() subObjTups = [] removalshapes = [] if obj.Base: PathLog.debug("base items exist. Processing... ") for base in obj.Base: PathLog.debug("obj.Base item: {}".format(base)) # Check if all subs are faces allSubsFaceType = True Faces = [] for sub in base[1]: if "Face" in sub: face = getattr(base[0].Shape, sub) Faces.append(face) subObjTups.append((sub, face)) else: allSubsFaceType = False break if len(Faces) == 0: allSubsFaceType = False if (allSubsFaceType is True and obj.HandleMultipleFeatures == "Collectively"): (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) if obj.FinalDepth.Value < fzmin: PathLog.warning( translate( "PathPocket", "Final depth set below ZMin of face(s) selected.", )) if (obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True): pocketTup = self.calculateAdaptivePocket( obj, base, subObjTups) if pocketTup is not False: obj.removalshape = pocketTup[0] removalshapes.append( pocketTup) # (shape, isHole, detail) else: shape = Part.makeCompound(Faces) env = PathUtils.getEnvelope( base[0].Shape, subshape=shape, depthparams=self.depthparams) rawRemovalShape = env.cut(base[0].Shape) faceExtrusions = [ f.extrude(FreeCAD.Vector(0.0, 0.0, 1.0)) for f in Faces ] obj.removalshape = _identifyRemovalSolids( rawRemovalShape, faceExtrusions) removalshapes.append( (obj.removalshape, False, "3DPocket")) # (shape, isHole, detail) else: for sub in base[1]: if "Face" in sub: shape = Part.makeCompound( [getattr(base[0].Shape, sub)]) else: edges = [ getattr(base[0].Shape, sub) for sub in base[1] ] shape = Part.makeFace(edges, "Part::FaceMakerSimple") env = PathUtils.getEnvelope( base[0].Shape, subshape=shape, depthparams=self.depthparams) rawRemovalShape = env.cut(base[0].Shape) faceExtrusions = [ shape.extrude(FreeCAD.Vector(0.0, 0.0, 1.0)) ] obj.removalshape = _identifyRemovalSolids( rawRemovalShape, faceExtrusions) removalshapes.append( (obj.removalshape, False, "3DPocket")) else: # process the job base object as a whole PathLog.debug("processing the whole job base object") for base in self.model: if obj.ProcessStockArea is True: job = PathUtils.findParentJob(obj) stockEnvShape = PathUtils.getEnvelope( job.Stock.Shape, subshape=None, depthparams=self.depthparams) rawRemovalShape = stockEnvShape.cut(base.Shape) else: env = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthparams) rawRemovalShape = env.cut(base.Shape) # Identify target removal shapes after cutting envelope with base shape removalSolids = [ s for s in rawRemovalShape.Solids if PathGeom.isRoughly( s.BoundBox.ZMax, rawRemovalShape.BoundBox.ZMax) ] # Fuse multiple solids if len(removalSolids) > 1: seed = removalSolids[0] for tt in removalSolids[1:]: fusion = seed.fuse(tt) seed = fusion removalShape = seed else: removalShape = removalSolids[0] obj.removalshape = removalShape removalshapes.append((obj.removalshape, False, "3DPocket")) return removalshapes
def isDrillable(obj, candidate, tooldiameter=None, includePartials=False): """ Checks candidates to see if they can be drilled. Candidates can be either faces - circular or cylindrical or circular edges. The tooldiameter can be optionally passed. if passed, the check will return False for any holes smaller than the tooldiameter. obj=Shape candidate = Face or Edge tooldiameter=float """ PathLog.track('obj: {} candidate: {} tooldiameter {}'.format(obj, candidate, tooldiameter)) drillable = False try: if candidate.ShapeType == 'Face': face = candidate # eliminate flat faces if (round(face.ParameterRange[0], 8) == 0.0) and (round(face.ParameterRange[1], 8) == round(math.pi * 2, 8)): for edge in face.Edges: # Find seam edge and check if aligned to Z axis. if (isinstance(edge.Curve, Part.Line)): PathLog.debug("candidate is a circle") v0 = edge.Vertexes[0].Point v1 = edge.Vertexes[1].Point #check if the cylinder seam is vertically aligned. Eliminate tilted holes if (numpy.isclose(v1.sub(v0).x, 0, rtol=1e-05, atol=1e-06)) and \ (numpy.isclose(v1.sub(v0).y, 0, rtol=1e-05, atol=1e-06)): drillable = True # vector of top center lsp = Vector(face.BoundBox.Center.x, face.BoundBox.Center.y, face.BoundBox.ZMax) # vector of bottom center lep = Vector(face.BoundBox.Center.x, face.BoundBox.Center.y, face.BoundBox.ZMin) # check if the cylindrical 'lids' are inside the base # object. This eliminates extruded circles but allows # actual holes. if obj.isInside(lsp, 1e-6, False) or obj.isInside(lep, 1e-6, False): PathLog.track("inside check failed. lsp: {} lep: {}".format(lsp,lep)) drillable = False # eliminate elliptical holes elif not hasattr(face.Surface, "Radius"): PathLog.debug("candidate face has no radius attribute") drillable = False else: if tooldiameter is not None: drillable = face.Surface.Radius >= tooldiameter/2 else: drillable = True elif type(face.Surface) == Part.Plane and PathGeom.pointsCoincide(face.Surface.Axis, FreeCAD.Vector(0,0,1)): if len(face.Edges) == 1 and type(face.Edges[0].Curve) == Part.Circle: center = face.Edges[0].Curve.Center if obj.isInside(center, 1e-6, False): if tooldiameter is not None: drillable = face.Edges[0].Curve.Radius >= tooldiameter/2 else: drillable = True else: for edge in candidate.Edges: if isinstance(edge.Curve, Part.Circle) and (includePartials or edge.isClosed()): PathLog.debug("candidate is a circle or ellipse") if not hasattr(edge.Curve, "Radius"): PathLog.debug("No radius. Ellipse.") drillable = False else: PathLog.debug("Has Radius, Circle") if tooldiameter is not None: drillable = edge.Curve.Radius >= tooldiameter/2 if not drillable: FreeCAD.Console.PrintMessage( "Found a drillable hole with diameter: {}: " "too small for the current tool with " "diameter: {}".format(edge.Curve.Radius*2, tooldiameter)) else: drillable = True PathLog.debug("candidate is drillable: {}".format(drillable)) except Exception as ex: PathLog.warning(translate("PathUtils", "Issue determine drillability: {}").format(ex)) return drillable
def process_nonloop_sublist(self, obj, base, sub): '''process_nonloop_sublist(obj, sub)... Process sublist with non-looped set of features when rotation is enabled. ''' rtn = False face = base.Shape.getElement(sub) if sub[:4] != 'Face': if face.ShapeType == 'Edge': edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([face]))) face = edgToFace else: ignoreSub = base.Name + '.' + sub PathLog.error( translate( 'Path', "Selected feature is not a Face. Ignoring: {}".format( ignoreSub))) return False (norm, surf) = self.getFaceNormAndSurf(face) (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.debug("initial rotational analysis: {}".format(praInfo)) clnBase = base faceIA = clnBase.Shape.getElement(sub) if faceIA.ShapeType == 'Edge': edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) faceIA = edgToFace if rtn is True: faceNum = sub.replace('Face', '') PathLog.debug("initial applyRotationalAnalysis") (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNum) # Verify faces are correctly oriented - InverseAngle might be necessary faceIA = clnBase.Shape.getElement(sub) if faceIA.ShapeType == 'Edge': edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) faceIA = edgToFace (norm, surf) = self.getFaceNormAndSurf(faceIA) (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.debug("follow-up rotational analysis: {}".format(praInfo2)) isFaceUp = self.isFaceUp(clnBase, faceIA) PathLog.debug('... initial isFaceUp: {}'.format(isFaceUp)) if isFaceUp: rtn = False PathLog.debug('returning analysis: {}, {}'.format( praAngle, praAxis)) return (clnBase, [sub], angle, axis, clnStock) if round(abs(praAngle), 8) == 180.0: rtn = False if not isFaceUp: PathLog.debug('initial isFaceUp is False') angle = 0.0 # Eif if rtn: # initial rotation failed, attempt inverse rotation if user requests it PathLog.debug( translate("Path", "Face appears misaligned after initial rotation.") + ' 2') if obj.AttemptInverseAngle: PathLog.debug( translate("Path", "Applying inverse angle automatically.")) (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) else: if obj.InverseAngle: PathLog.debug( translate("Path", "Applying inverse angle manually.")) (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) else: msg = translate( "Path", "Consider toggling the 'InverseAngle' property and recomputing." ) PathLog.warning(msg) faceIA = clnBase.Shape.getElement(sub) if faceIA.ShapeType == 'Edge': edgToFace = Part.Face(Part.Wire(Part.__sortEdges__([faceIA]))) faceIA = edgToFace if not self.isFaceUp(clnBase, faceIA): angle += 180.0 # Normalize rotation angle if angle < 0.0: angle += 360.0 elif angle > 360.0: angle -= 360.0 return (clnBase, [sub], angle, axis, clnStock) if not self.warnDisabledAxis(obj, axis): PathLog.debug(str(sub) + ": No rotation used") axis = 'X' angle = 0.0 stock = PathUtils.findParentJob(obj).Stock return (base, [sub], angle, axis, stock)
def warn(msg): PathLog.warning(msg)
def _extractPathWire(self, obj, base, flatWire, cutShp): PathLog.debug('_extractPathWire()') subLoops = list() rtnWIRES = list() osWrIdxs = list() subDistFactor = 1.0 # Raise to include sub wires at greater distance from original fdv = obj.FinalDepth.Value wire = flatWire lstVrtIdx = len(wire.Vertexes) - 1 lstVrt = wire.Vertexes[lstVrtIdx] frstVrt = wire.Vertexes[0] cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv) cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv) pl = FreeCAD.Placement() pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) pl.Base = FreeCAD.Vector(0, 0, 0) # Calculate offset shape, containing cut region ofstShp = self._extractFaceOffset(obj, cutShp, False) # CHECK for ZERO area of offset shape try: osArea = ofstShp.Area except Exception as ee: PathLog.error('No area to offset shape returned.') 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) numOSWires = len(ofstShp.Wires) for w in range(0, numOSWires): osWrIdxs.append(w) # Identify two vertexes for dividing offset loop NEAR0 = self._findNearestVertex(ofstShp, cent0) min0i = 0 min0 = NEAR0[0][4] for n in range(0, len(NEAR0)): N = NEAR0[n] if N[4] < min0: 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) NEAR1 = self._findNearestVertex(ofstShp, cent1) min1i = 0 min1 = NEAR1[0][4] for n in range(0, len(NEAR1)): N = NEAR1[n] if N[4] < min1: 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) if w0 != w1: PathLog.warning( 'Offset wire endpoint indexes are not equal - w0, w1: {}, {}'. format(w0, w1)) if PathLog.getLevel(PathLog.thisModule()) == 4: PathLog.debug('min0i is {}.'.format(min0i)) PathLog.debug('min1i is {}.'.format(min1i)) PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) PathLog.debug('NEAR0 is {}.'.format(NEAR0)) PathLog.debug('NEAR1 is {}.'.format(NEAR1)) mainWire = ofstShp.Wires[w0] # Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements if numOSWires > 1: # check all wires for proximity(children) to intersection tags tagsComList = list() for T in self.cutSideTags.Faces: tcom = T.CenterOfMass tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0) tagsComList.append(tv) subDist = self.ofstRadius * subDistFactor for w in osWrIdxs: if w != w0: cutSub = False VTXS = ofstShp.Wires[w].Vertexes for V in VTXS: v = FreeCAD.Vector(V.X, V.Y, 0.0) for t in tagsComList: if t.sub(v).Length < subDist: cutSub = True break if cutSub is True: break if cutSub is True: sub = Part.Wire( Part.__sortEdges__(ofstShp.Wires[w].Edges)) subLoops.append(sub) # Eif # Break offset loop into two wires - one of which is the desired profile path wire. (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]) edgs0 = list() edgs1 = list() for e in edgeIdxs0: edgs0.append(mainWire.Edges[e]) for e in edgeIdxs1: edgs1.append(mainWire.Edges[e]) part0 = Part.Wire(Part.__sortEdges__(edgs0)) part1 = Part.Wire(Part.__sortEdges__(edgs1)) # Determine which part is nearest original edge(s) distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0]) distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0]) if distToPart0 < distToPart1: rtnWIRES.append(part0) else: rtnWIRES.append(part1) rtnWIRES.extend(subLoops) return rtnWIRES
def clasifySub(self, bs, sub): face = bs.Shape.getElement(sub) if type(face.Surface) == Part.Plane: PathLog.debug('type() == Part.Plane') if PathGeom.isVertical(face.Surface.Axis): PathLog.debug(' -isVertical()') # it's a flat horizontal face self.horiz.append(face) return True elif PathGeom.isHorizontal(face.Surface.Axis): PathLog.debug(' -isHorizontal()') self.vert.append(face) return True else: return False elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical( face.Surface.Axis): PathLog.debug('type() == Part.Cylinder') # vertical cylinder wall if any(e.isClosed() for e in face.Edges): PathLog.debug(' -e.isClosed()') # complete cylinder circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center) disk = Part.Face(Part.Wire(circle)) disk.translate( FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin)) self.horiz.append(disk) return True else: PathLog.debug(' -none isClosed()') # partial cylinder wall self.vert.append(face) return True elif type(face.Surface) == Part.SurfaceOfExtrusion: # extrusion wall PathLog.debug('type() == Part.SurfaceOfExtrusion') # Attempt to extract planar face from surface of extrusion (planar, useFace) = planarFaceFromExtrusionEdges(face, trans=True) # Save face object to self.horiz for processing or display error if planar is True: uFace = FreeCAD.ActiveDocument.getObject(useFace) self.horiz.append(uFace.Shape.Faces[0]) msg = translate( 'Path', "<b>Verify depth of pocket for '{}'.</b>".format(sub)) msg += translate( 'Path', "\n<br>Pocket is based on extruded surface.") msg += translate( 'Path', "\n<br>Bottom of pocket might be non-planar and/or not normal to spindle axis." ) msg += translate( 'Path', "\n<br>\n<br><i>3D pocket bottom is NOT available in this operation</i>." ) PathLog.warning(msg) # title = translate('Path', 'Depth Warning') # self.guiMessage(title, msg, False) else: PathLog.error( translate( "Path", "Failed to create a planar face from edges in {}.". format(sub))) else: PathLog.debug(' -type(face.Surface): {}'.format( type(face.Surface))) return False
def calculateAdaptivePocket(self, obj, base, subObjTups): '''calculateAdaptivePocket(obj, base, subObjTups) Orient multiple faces around common facial center of mass. Identify edges that are connections for adjacent faces. Attempt to separate unconnected edges into top and bottom loops of the pocket. Trim the top and bottom of the pocket if available and requested. return: tuple with pocket shape information''' low = [] high = [] removeList = [] Faces = [] allEdges = [] makeHighFace = 0 tryNonPlanar = False isHighFacePlanar = True isLowFacePlanar = True faceType = 0 for (sub, face) in subObjTups: Faces.append(face) # identify max and min face heights for top loop (zmin, zmax) = self.getMinMaxOfFaces(Faces) # Order faces around common center of mass subObjTups = self.orderFacesAroundCenterOfMass(subObjTups) # find connected edges and map to edge names of base (connectedEdges, touching) = self.findSharedEdges(subObjTups) (low, high) = self.identifyUnconnectedEdges(subObjTups, touching) if len(high) > 0 and obj.AdaptivePocketStart is True: # attempt planar face with top edges of pocket allEdges = [] makeHighFace = 0 tryNonPlanar = False for (sub, face, ei) in high: allEdges.append(face.Edges[ei]) (hzmin, hzmax) = self.getMinMaxOfFaces(allEdges) try: highFaceShape = Part.Face( Part.Wire(Part.__sortEdges__(allEdges))) except Exception as ee: PathLog.warning(ee) PathLog.error( translate( "Path", "A planar adaptive start is unavailable. The non-planar will be attempted." )) tryNonPlanar = True else: makeHighFace = 1 if tryNonPlanar is True: try: highFaceShape = Part.makeFilledFace( Part.__sortEdges__(allEdges)) # NON-planar face method except Exception as eee: PathLog.warning(eee) PathLog.error( translate( "Path", "The non-planar adaptive start is also unavailable." ) + "(1)") isHighFacePlanar = False else: makeHighFace = 2 if makeHighFace > 0: FreeCAD.ActiveDocument.addObject('Part::Feature', 'topEdgeFace') highFace = FreeCAD.ActiveDocument.ActiveObject highFace.Shape = highFaceShape removeList.append(highFace.Name) # verify non-planar face is within high edge loop Z-boundaries if makeHighFace == 2: mx = hzmax + obj.StepDown.Value mn = hzmin - obj.StepDown.Value if highFace.Shape.BoundBox.ZMax > mx or highFace.Shape.BoundBox.ZMin < mn: PathLog.warning("ZMaxDiff: {}; ZMinDiff: {}".format( highFace.Shape.BoundBox.ZMax - mx, highFace.Shape.BoundBox.ZMin - mn)) PathLog.error( translate( "Path", "The non-planar adaptive start is also unavailable." ) + "(2)") isHighFacePlanar = False makeHighFace = 0 else: isHighFacePlanar = False if len(low) > 0 and obj.AdaptivePocketFinish is True: # attempt planar face with bottom edges of pocket allEdges = [] for (sub, face, ei) in low: allEdges.append(face.Edges[ei]) # (lzmin, lzmax) = self.getMinMaxOfFaces(allEdges) try: lowFaceShape = Part.Face( Part.Wire(Part.__sortEdges__(allEdges))) # lowFaceShape = Part.makeFilledFace(Part.__sortEdges__(allEdges)) # NON-planar face method except Exception as ee: PathLog.error(ee) PathLog.error("An adaptive finish is unavailable.") isLowFacePlanar = False else: FreeCAD.ActiveDocument.addObject('Part::Feature', 'bottomEdgeFace') lowFace = FreeCAD.ActiveDocument.ActiveObject lowFace.Shape = lowFaceShape removeList.append(lowFace.Name) else: isLowFacePlanar = False # Start with a regular pocket envelope strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value cuts = [] starts = [] finals = [] starts.append(obj.StartDepth.Value) finals.append(zmin) if obj.AdaptivePocketStart is True or len(subObjTups) == 1: strDep = zmax + obj.StepDown.Value starts.append(zmax + obj.StepDown.Value) finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 depthparams = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=strDep, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=finDep, user_depths=None) shape = Part.makeCompound(Faces) env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=depthparams) cuts.append(env.cut(base[0].Shape)) # Might need to change to .cut(job.Stock.Shape) if pocket has no bottom # job = PathUtils.findParentJob(obj) # envBody = env.cut(job.Stock.Shape) if isHighFacePlanar is True and len(subObjTups) > 1: starts.append(hzmax + obj.StepDown.Value) # make shape to trim top of reg pocket strDep1 = obj.StartDepth.Value + (hzmax - hzmin) if makeHighFace == 1: # Planar face finDep1 = highFace.Shape.BoundBox.ZMin + obj.StepDown.Value else: # Non-Planar face finDep1 = hzmin + obj.StepDown.Value depthparams1 = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=strDep1, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=finDep1, user_depths=None) envTop = PathUtils.getEnvelope(base[0].Shape, subshape=highFace.Shape, depthparams=depthparams1) cbi = len(cuts) - 1 cuts.append(cuts[cbi].cut(envTop)) if isLowFacePlanar is True and len(subObjTups) > 1: # make shape to trim top of pocket if makeHighFace == 1: # Planar face strDep2 = lowFace.Shape.BoundBox.ZMax else: # Non-Planar face strDep2 = hzmax finDep2 = obj.FinalDepth.Value depthparams2 = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=strDep2, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=finDep2, user_depths=None) envBottom = PathUtils.getEnvelope(base[0].Shape, subshape=lowFace.Shape, depthparams=depthparams2) cbi = len(cuts) - 1 cuts.append(cuts[cbi].cut(envBottom)) # package pocket details into tuple sdi = len(starts) - 1 fdi = len(finals) - 1 cbi = len(cuts) - 1 pocket = (cuts[cbi], False, '3DPocket', 0.0, 'X', starts[sdi], finals[fdi]) if FreeCAD.GuiUp: import FreeCADGui for rn in removeList: FreeCADGui.ActiveDocument.getObject(rn).Visibility = False for rn in removeList: FreeCAD.ActiveDocument.getObject(rn).purgeTouched() self.tempObjectNames.append(rn) return pocket
def areaOpShapes(self, obj): """areaOpShapes(obj) ... return shapes representing the solids to be removed.""" PathLog.track() subObjTups = [] removalshapes = [] if obj.Base: PathLog.debug("base items exist. Processing... ") for base in obj.Base: PathLog.debug("obj.Base item: {}".format(base)) # Check if all subs are faces allSubsFaceType = True Faces = [] for sub in base[1]: if "Face" in sub: face = getattr(base[0].Shape, sub) Faces.append(face) subObjTups.append((sub, face)) else: allSubsFaceType = False break if len(Faces) == 0: allSubsFaceType = False if (allSubsFaceType is True and obj.HandleMultipleFeatures == "Collectively"): (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) if obj.FinalDepth.Value < fzmin: PathLog.warning( translate( "PathPocket", "Final depth set below ZMin of face(s) selected.", )) if (obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True): pocketTup = self.calculateAdaptivePocket( obj, base, subObjTups) if pocketTup is not False: obj.removalshape = pocketTup[0] removalshapes.append( pocketTup) # (shape, isHole, detail) else: shape = Part.makeCompound(Faces) env = PathUtils.getEnvelope( base[0].Shape, subshape=shape, depthparams=self.depthparams) obj.removalshape = env.cut(base[0].Shape) # obj.removalshape.tessellate(0.1) removalshapes.append( (obj.removalshape, False, "3DPocket")) # (shape, isHole, detail) else: for sub in base[1]: if "Face" in sub: shape = Part.makeCompound( [getattr(base[0].Shape, sub)]) else: edges = [ getattr(base[0].Shape, sub) for sub in base[1] ] shape = Part.makeFace(edges, "Part::FaceMakerSimple") env = PathUtils.getEnvelope( base[0].Shape, subshape=shape, depthparams=self.depthparams) obj.removalshape = env.cut(base[0].Shape) # obj.removalshape.tessellate(0.1) removalshapes.append( (obj.removalshape, False, "3DPocket")) else: # process the job base object as a whole PathLog.debug("processing the whole job base object") for base in self.model: if obj.ProcessStockArea is True: job = PathUtils.findParentJob(obj) stockEnvShape = PathUtils.getEnvelope( job.Stock.Shape, subshape=None, depthparams=self.depthparams) obj.removalshape = stockEnvShape.cut(base.Shape) # obj.removalshape.tessellate(0.1) else: env = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthparams) obj.removalshape = env.cut(base.Shape) # obj.removalshape.tessellate(0.1) removalshapes.append((obj.removalshape, False, "3DPocket")) return removalshapes
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... return shapes representing the solids to be removed.''' PathLog.track() subObjTups = [] removalshapes = [] if obj.Base: PathLog.debug("base items exist. Processing... ") for base in obj.Base: PathLog.debug("obj.Base item: {}".format(base)) # Check if all subs are faces allSubsFaceType = True Faces = [] for sub in base[1]: if "Face" in sub: face = getattr(base[0].Shape, sub) Faces.append(face) subObjTups.append((sub, face)) else: allSubsFaceType = False break if len(Faces) == 0: allSubsFaceType = False if allSubsFaceType is True and obj.HandleMultipleFeatures == 'Collectively': (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) if obj.FinalDepth.Value < fzmin: PathLog.warning( translate( 'PathPocket', 'Final depth set below ZMin of face(s) selected.' )) ''' if obj.OpFinalDepth == obj.FinalDepth: obj.FinalDepth.Value = fzmin finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 self.depthparams = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=obj.StartDepth.Value, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=fzmin, user_depths=None) PathLog.info("Updated obj.FinalDepth.Value and self.depthparams to zmin: {}".format(fzmin)) ''' if obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True: pocketTup = self.calculateAdaptivePocket( obj, base, subObjTups) if pocketTup is not False: removalshapes.append( pocketTup ) # (shape, isHole, sub, angle, axis, strDep, finDep) else: strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value shape = Part.makeCompound(Faces) env = PathUtils.getEnvelope( base[0].Shape, subshape=shape, depthparams=self.depthparams) obj.removalshape = env.cut(base[0].Shape) obj.removalshape.tessellate(0.1) # (shape, isHole, sub, angle, axis, strDep, finDep) removalshapes.append( (obj.removalshape, False, '3DPocket', 0.0, 'X', strDep, finDep)) else: for sub in base[1]: if "Face" in sub: shape = Part.makeCompound( [getattr(base[0].Shape, sub)]) else: edges = [ getattr(base[0].Shape, sub) for sub in base[1] ] shape = Part.makeFace(edges, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope( base[0].Shape, subshape=shape, depthparams=self.depthparams) obj.removalshape = env.cut(base[0].Shape) obj.removalshape.tessellate(0.1) removalshapes.append((obj.removalshape, False)) else: # process the job base object as a whole PathLog.debug("processing the whole job base object") strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value # recomputeDepthparams = False for base in self.model: ''' if obj.OpFinalDepth == obj.FinalDepth: if base.Shape.BoundBox.ZMin < obj.FinalDepth.Value: obj.FinalDepth.Value = base.Shape.BoundBox.ZMin finDep = base.Shape.BoundBox.ZMin recomputeDepthparams = True PathLog.info("Updated obj.FinalDepth.Value to {}".format(finDep)) if obj.OpStartDepth == obj.StartDepth: if base.Shape.BoundBox.ZMax > obj.StartDepth.Value: obj.StartDepth.Value = base.Shape.BoundBox.ZMax finDep = base.Shape.BoundBox.ZMax recomputeDepthparams = True PathLog.info("Updated obj.StartDepth.Value to {}".format(strDep)) if recomputeDepthparams is True: finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 self.depthparams = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=obj.StartDepth.Value, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=obj.FinalDepth.Value, user_depths=None) recomputeDepthparams = False ''' if obj.ProcessStockArea is True: job = PathUtils.findParentJob(obj) ''' finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 depthparams = PathUtils.depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=obj.StartDepth.Value, step_down=obj.StepDown.Value, z_finish_step=finish_step, final_depth=base.Shape.BoundBox.ZMin, user_depths=None) stockEnvShape = PathUtils.getEnvelope(job.Stock.Shape, subshape=None, depthparams=depthparams) ''' stockEnvShape = PathUtils.getEnvelope( job.Stock.Shape, subshape=None, depthparams=self.depthparams) obj.removalshape = stockEnvShape.cut(base.Shape) obj.removalshape.tessellate(0.1) else: env = PathUtils.getEnvelope(base.Shape, subshape=None, depthparams=self.depthparams) obj.removalshape = env.cut(base.Shape) obj.removalshape.tessellate(0.1) removalshapes.append((obj.removalshape, False, '3DPocket', 0.0, 'X', strDep, finDep)) return removalshapes
def execute(self): if not self.baseOp or not self.baseOp.isDerivedFrom('Path::Feature') or not self.baseOp.Path: return None if len(self.baseOp.Path.Commands) == 0: PathLog.warning("No Path Commands for %s" % self.baseOp.Label) return [] tc = PathDressup.toolController(self.baseOp) self.safeHeight = float(PathUtil.opProperty(self.baseOp, 'SafeHeight')) self.clearanceHeight = float(PathUtil.opProperty(self.baseOp, 'ClearanceHeight')) self.strG1ZsafeHeight = Path.Command('G1', {'Z': self.safeHeight, 'F': tc.VertFeed.Value}) self.strG0ZclearanceHeight = Path.Command('G0', {'Z': self.clearanceHeight}) cmd = self.baseOp.Path.Commands[0] pos = cmd.Placement.Base # bogus m/c position to create first edge bogusX = True bogusY = True commands = [cmd] lastExit = None for cmd in self.baseOp.Path.Commands[1:]: if cmd.Name in PathGeom.CmdMoveAll: if bogusX == True : bogusX = ( 'X' not in cmd.Parameters ) if bogusY : bogusY = ( 'Y' not in cmd.Parameters ) edge = PathGeom.edgeForCmd(cmd, pos) if edge: inside = edge.common(self.boundary).Edges outside = edge.cut(self.boundary).Edges if not self.inside: # UI "inside boundary" param tmp = inside inside = outside outside = tmp # it's really a shame that one cannot trust the sequence and/or # orientation of edges if 1 == len(inside) and 0 == len(outside): PathLog.track(_vstr(pos), _vstr(lastExit), ' + ', cmd) # cmd fully included by boundary if lastExit: if not ( bogusX or bogusY ) : # don't insert false paths based on bogus m/c position commands.extend(self.boundaryCommands(lastExit, pos, tc.VertFeed.Value)) lastExit = None commands.append(cmd) pos = PathGeom.commandEndPoint(cmd, pos) elif 0 == len(inside) and 1 == len(outside): PathLog.track(_vstr(pos), _vstr(lastExit), ' - ', cmd) # cmd fully excluded by boundary if not lastExit: lastExit = pos pos = PathGeom.commandEndPoint(cmd, pos) else: PathLog.track(_vstr(pos), _vstr(lastExit), len(inside), len(outside), cmd) # cmd pierces boundary while inside or outside: ie = [e for e in inside if PathGeom.edgeConnectsTo(e, pos)] PathLog.track(ie) if ie: e = ie[0] LastPt = e.valueAt(e.LastParameter) flip = PathGeom.pointsCoincide(pos, LastPt) newPos = e.valueAt(e.FirstParameter) if flip else LastPt # inside edges are taken at this point (see swap of inside/outside # above - so we can just connect the dots ... if lastExit: if not ( bogusX or bogusY ) : commands.extend(self.boundaryCommands(lastExit, pos, tc.VertFeed.Value)) lastExit = None PathLog.track(e, flip) if not ( bogusX or bogusY ) : # don't insert false paths based on bogus m/c position commands.extend(PathGeom.cmdsForEdge(e, flip, False, 50, tc.HorizFeed.Value, tc.VertFeed.Value)) inside.remove(e) pos = newPos lastExit = newPos else: oe = [e for e in outside if PathGeom.edgeConnectsTo(e, pos)] PathLog.track(oe) if oe: e = oe[0] ptL = e.valueAt(e.LastParameter) flip = PathGeom.pointsCoincide(pos, ptL) newPos = e.valueAt(e.FirstParameter) if flip else ptL # outside edges are never taken at this point (see swap of # inside/outside above) - so just move along ... outside.remove(e) pos = newPos else: PathLog.error('huh?') import Part Part.show(Part.Vertex(pos), 'pos') for e in inside: Part.show(e, 'ei') for e in outside: Part.show(e, 'eo') raise Exception('This is not supposed to happen') # Eif # Eif # Ewhile # Eif # pos = PathGeom.commandEndPoint(cmd, pos) # Eif else: PathLog.track('no-move', cmd) commands.append(cmd) if lastExit: commands.extend(self.boundaryCommands(lastExit, None, tc.VertFeed.Value)) lastExit = None PathLog.track(commands) return Path.Path(commands)
def execute(self, obj): if not obj.Base or not obj.Base.isDerivedFrom( 'Path::Feature') or not obj.Base.Path: return tc = PathDressup.toolController(obj.Base) if len(obj.Base.Path.Commands) > 0: self.safeHeight = float(PathUtil.opProperty( obj.Base, 'SafeHeight')) self.clearanceHeight = float( PathUtil.opProperty(obj.Base, 'ClearanceHeight')) boundary = obj.Stock.Shape cmd = obj.Base.Path.Commands[0] pos = cmd.Placement.Base commands = [cmd] lastExit = None for cmd in obj.Base.Path.Commands[1:]: if cmd.Name in PathGeom.CmdMoveAll: edge = PathGeom.edgeForCmd(cmd, pos) if edge: inside = edge.common(boundary).Edges outside = edge.cut(boundary).Edges if not obj.Inside: t = inside inside = outside outside = t # it's really a shame that one cannot trust the sequence and/or # orientation of edges if 1 == len(inside) and 0 == len(outside): PathLog.track(_vstr(pos), _vstr(lastExit), ' + ', cmd) # cmd fully included by boundary if lastExit: commands.extend( self.boundaryCommands( obj, lastExit, pos, tc.VertFeed.Value)) lastExit = None commands.append(cmd) pos = PathGeom.commandEndPoint(cmd, pos) elif 0 == len(inside) and 1 == len(outside): PathLog.track(_vstr(pos), _vstr(lastExit), ' - ', cmd) # cmd fully excluded by boundary if not lastExit: lastExit = pos pos = PathGeom.commandEndPoint(cmd, pos) else: PathLog.track(_vstr(pos), _vstr(lastExit), len(inside), len(outside), cmd) # cmd pierces boundary while inside or outside: ie = [ e for e in inside if PathGeom.edgeConnectsTo(e, pos) ] PathLog.track(ie) if ie: e = ie[0] ptL = e.valueAt(e.LastParameter) flip = PathGeom.pointsCoincide(pos, ptL) newPos = e.valueAt( e.FirstParameter) if flip else ptL # inside edges are taken at this point (see swap of inside/outside # above - so we can just connect the dots ... if lastExit: commands.extend( self.boundaryCommands( obj, lastExit, pos, tc.VertFeed.Value)) lastExit = None PathLog.track(e, flip) commands.extend( PathGeom.cmdsForEdge( e, flip, False, 50, tc.HorizFeed.Value, tc.VertFeed.Value) ) # add missing HorizFeed to G2 paths inside.remove(e) pos = newPos lastExit = newPos else: oe = [ e for e in outside if PathGeom.edgeConnectsTo(e, pos) ] PathLog.track(oe) if oe: e = oe[0] ptL = e.valueAt(e.LastParameter) flip = PathGeom.pointsCoincide( pos, ptL) newPos = e.valueAt( e.FirstParameter) if flip else ptL # outside edges are never taken at this point (see swap of # inside/outside above) - so just move along ... outside.remove(e) pos = newPos else: PathLog.error('huh?') import Part Part.show(Part.Vertex(pos), 'pos') for e in inside: Part.show(e, 'ei') for e in outside: Part.show(e, 'eo') raise Exception( 'This is not supposed to happen') # Eif # Eif # Ewhile # Eif # pos = PathGeom.commandEndPoint(cmd, pos) # Eif else: PathLog.track('no-move', cmd) commands.append(cmd) if lastExit: commands.extend( self.boundaryCommands(obj, lastExit, None, tc.VertFeed.Value)) lastExit = None else: PathLog.warning("No Path Commands for %s" % obj.Base.Label) commands = [] PathLog.track(commands) obj.Path = Path.Path(commands)
def CreateFromTemplate(job, template): if template.get('version') and 1 == int(template['version']): stockType = template.get('create') if stockType: placement = None posX = template.get('posX') posY = template.get('posY') posZ = template.get('posZ') rotX = template.get('rotX') rotY = template.get('rotY') rotZ = template.get('rotZ') rotW = template.get('rotW') if posX is not None and posY is not None and posZ is not None and rotX is not None and rotY is not None and rotZ is not None and rotW is not None: pos = FreeCAD.Vector(float(posX), float(posY), float(posZ)) rot = FreeCAD.Rotation(float(rotX), float(rotY), float(rotZ), float(rotW)) placement = FreeCAD.Placement(pos, rot) elif posX is not None or posY is not None or posZ is not None or rotX is not None or rotY is not None or rotZ is not None or rotW is not None: PathLog.warning( translate( 'PathStock', 'Corrupted or incomplete placement information in template - ignoring' )) if stockType == StockType.FromBase: xneg = template.get('xneg') xpos = template.get('xpos') yneg = template.get('yneg') ypos = template.get('ypos') zneg = template.get('zneg') zpos = template.get('zpos') neg = None pos = None if xneg is not None and xpos is not None and yneg is not None and ypos is not None and zneg is not None and zpos is not None: neg = FreeCAD.Vector( FreeCAD.Units.Quantity(xneg).Value, FreeCAD.Units.Quantity(yneg).Value, FreeCAD.Units.Quantity(zneg).Value) pos = FreeCAD.Vector( FreeCAD.Units.Quantity(xpos).Value, FreeCAD.Units.Quantity(ypos).Value, FreeCAD.Units.Quantity(zpos).Value) elif xneg is not None or xpos is not None or yneg is not None or ypos is not None or zneg is not None or zpos is not None: PathLog.error( translate( 'PathStock', 'Corrupted or incomplete specification for creating stock from base - ignoring extent' )) return CreateFromBase(job, neg, pos, placement) if stockType == StockType.CreateBox: PathLog.track(' create box') length = template.get('length') width = template.get('width') height = template.get('height') extent = None if length is not None and width is not None and height is not None: PathLog.track(' have extent') extent = FreeCAD.Vector( FreeCAD.Units.Quantity(length).Value, FreeCAD.Units.Quantity(width).Value, FreeCAD.Units.Quantity(height).Value) elif length is not None or width is not None or height is not None: PathLog.error( translate( 'PathStock', 'Corrupted or incomplete size for creating a stock box - ignoring size' )) else: PathLog.track( " take placement (%s) and extent (%s) from model" % (placement, extent)) return CreateBox(job, extent, placement) if stockType == StockType.CreateCylinder: radius = template.get('radius') height = template.get('height') if radius is not None and height is not None: pass elif radius is not None or height is not None: radius = None height = None PathLog.error( translate( 'PathStock', 'Corrupted or incomplete size for creating a stock cylinder - ignoring size' )) return CreateCylinder(job, radius, height, placement) PathLog.error( translate('PathStock', 'Unsupported stock type named {}').format(stockType)) else: PathLog.error( translate('PathStock', 'Unsupported PathStock template version {}').format( template.get('version'))) return None
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... return shapes representing the solids to be removed.''' PathLog.track() PathLog.debug("----- areaOpShapes() in PathPocketShape.py") baseSubsTuples = [] subCount = 0 allTuples = [] def planarFaceFromExtrusionEdges(face, trans): useFace = 'useFaceName' minArea = 0.0 fCnt = 0 clsd = [] planar = False # Identify closed edges for edg in face.Edges: if edg.isClosed(): PathLog.debug(' -e.isClosed()') clsd.append(edg) planar = True # Attempt to create planar faces and select that with smallest area for use as pocket base if planar is True: planar = False for edg in clsd: fCnt += 1 fName = sub + '_face_' + str(fCnt) # Create planar face from edge mFF = Part.Face(Part.Wire(Part.__sortEdges__([edg]))) if mFF.isNull(): PathLog.debug('Face(Part.Wire()) failed') else: if trans is True: mFF.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - mFF.BoundBox.ZMin)) if FreeCAD.ActiveDocument.getObject(fName): FreeCAD.ActiveDocument.removeObject(fName) tmpFace = FreeCAD.ActiveDocument.addObject('Part::Feature', fName).Shape = mFF tmpFace = FreeCAD.ActiveDocument.getObject(fName) tmpFace.purgeTouched() if minArea == 0.0: minArea = tmpFace.Shape.Face1.Area useFace = fName planar = True elif tmpFace.Shape.Face1.Area < minArea: minArea = tmpFace.Shape.Face1.Area FreeCAD.ActiveDocument.removeObject(useFace) useFace = fName else: FreeCAD.ActiveDocument.removeObject(fName) if useFace != 'useFaceName': self.useTempJobClones(useFace) return (planar, useFace) def clasifySub(self, bs, sub): face = bs.Shape.getElement(sub) if type(face.Surface) == Part.Plane: PathLog.debug('type() == Part.Plane') if PathGeom.isVertical(face.Surface.Axis): PathLog.debug(' -isVertical()') # it's a flat horizontal face self.horiz.append(face) return True elif PathGeom.isHorizontal(face.Surface.Axis): PathLog.debug(' -isHorizontal()') self.vert.append(face) return True else: return False elif type(face.Surface) == Part.Cylinder and PathGeom.isVertical(face.Surface.Axis): PathLog.debug('type() == Part.Cylinder') # vertical cylinder wall if any(e.isClosed() for e in face.Edges): PathLog.debug(' -e.isClosed()') # complete cylinder circle = Part.makeCircle(face.Surface.Radius, face.Surface.Center) disk = Part.Face(Part.Wire(circle)) disk.translate(FreeCAD.Vector(0, 0, face.BoundBox.ZMin - disk.BoundBox.ZMin)) self.horiz.append(disk) return True else: PathLog.debug(' -none isClosed()') # partial cylinder wall self.vert.append(face) return True elif type(face.Surface) == Part.SurfaceOfExtrusion: # extrusion wall PathLog.debug('type() == Part.SurfaceOfExtrusion') # Attempt to extract planar face from surface of extrusion (planar, useFace) = planarFaceFromExtrusionEdges(face, trans=True) # Save face object to self.horiz for processing or display error if planar is True: uFace = FreeCAD.ActiveDocument.getObject(useFace) self.horiz.append(uFace.Shape.Faces[0]) msg = translate('Path', "<b>Verify depth of pocket for '{}'.</b>".format(sub)) msg += translate('Path', "\n<br>Pocket is based on extruded surface.") msg += translate('Path', "\n<br>Bottom of pocket might be non-planar and/or not normal to spindle axis.") msg += translate('Path', "\n<br>\n<br><i>3D pocket bottom is NOT available in this operation</i>.") PathLog.warning(msg) # title = translate('Path', 'Depth Warning') # self.guiMessage(title, msg, False) else: PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub))) else: PathLog.debug(' -type(face.Surface): {}'.format(type(face.Surface))) return False if obj.Base: PathLog.debug('Processing... obj.Base') self.removalshapes = [] # pylint: disable=attribute-defined-outside-init if obj.EnableRotation == 'Off': stock = PathUtils.findParentJob(obj).Stock for (base, subList) in obj.Base: baseSubsTuples.append((base, subList, 0.0, 'X', stock)) else: for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] isLoop = False # First, check all subs collectively for loop of faces if len(subsList) > 2: (isLoop, norm, surf) = self.checkForFacesLoop(base, subsList) if isLoop is True: PathLog.debug("Common Surface.Axis or normalAt() value found for loop faces.") rtn = False subCount += 1 (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.debug("angle: {}; axis: {}".format(angle, axis)) if rtn is True: faceNums = "" for f in subsList: faceNums += '_' + f.replace('Face', '') (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNums) # pylint: disable=unused-variable # Verify faces are correctly oriented - InverseAngle might be necessary PathLog.debug("Checking if faces are oriented correctly after rotation...") for sub in subsList: face = clnBase.Shape.getElement(sub) if type(face.Surface) == Part.Plane: if not PathGeom.isHorizontal(face.Surface.Axis): rtn = False PathLog.warning(translate("PathPocketShape", "Face appears to NOT be horizontal AFTER rotation applied.")) break if rtn is False: PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) if obj.InverseAngle is False: if obj.AttemptInverseAngle is True: (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) else: msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") PathLog.warning(msg) if angle < 0.0: angle += 360.0 tup = clnBase, subsList, angle, axis, clnStock else: if self.warnDisabledAxis(obj, axis) is False: PathLog.debug("No rotation used") axis = 'X' angle = 0.0 stock = PathUtils.findParentJob(obj).Stock tup = base, subsList, angle, axis, stock # Eif allTuples.append(tup) baseSubsTuples.append(tup) # Eif if isLoop is False: PathLog.debug(translate('Path', "Processing subs individually ...")) for sub in subsList: subCount += 1 if 'Face' in sub: rtn = False face = base.Shape.getElement(sub) if type(face.Surface) == Part.SurfaceOfExtrusion: # extrusion wall PathLog.debug('analyzing type() == Part.SurfaceOfExtrusion') # Attempt to extract planar face from surface of extrusion (planar, useFace) = planarFaceFromExtrusionEdges(face, trans=False) # Save face object to self.horiz for processing or display error if planar is True: base = FreeCAD.ActiveDocument.getObject(useFace) sub = 'Face1' PathLog.debug(' -successful face created: {}'.format(useFace)) else: PathLog.error(translate("Path", "Failed to create a planar face from edges in {}.".format(sub))) (norm, surf) = self.getFaceNormAndSurf(face) (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.debug("initial {}".format(praInfo)) if rtn is True: faceNum = sub.replace('Face', '') (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, faceNum) # Verify faces are correctly oriented - InverseAngle might be necessary faceIA = clnBase.Shape.getElement(sub) (norm, surf) = self.getFaceNormAndSurf(faceIA) (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.debug("follow-up {}".format(praInfo2)) if abs(praAngle) == 180.0: rtn = False if self.isFaceUp(clnBase, faceIA) is False: PathLog.debug('isFaceUp is False') angle -= 180.0 if rtn is True: PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) if obj.InverseAngle is False: if obj.AttemptInverseAngle is True: (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) else: msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") PathLog.warning(msg) if self.isFaceUp(clnBase, faceIA) is False: PathLog.debug('isFaceUp is False') angle += 180.0 else: PathLog.debug("Face appears to be oriented correctly.") if angle < 0.0: angle += 360.0 tup = clnBase, [sub], angle, axis, clnStock else: if self.warnDisabledAxis(obj, axis) is False: PathLog.debug(str(sub) + ": No rotation used") axis = 'X' angle = 0.0 stock = PathUtils.findParentJob(obj).Stock tup = base, [sub], angle, axis, stock # Eif allTuples.append(tup) baseSubsTuples.append(tup) else: ignoreSub = base.Name + '.' + sub PathLog.error(translate('Path', "Selected feature is not a Face. Ignoring: {}".format(ignoreSub))) for o in baseSubsTuples: self.horiz = [] # pylint: disable=attribute-defined-outside-init self.vert = [] # pylint: disable=attribute-defined-outside-init subBase = o[0] subsList = o[1] angle = o[2] axis = o[3] stock = o[4] for sub in subsList: if 'Face' in sub: if clasifySub(self, subBase, sub) is False: PathLog.error(translate('PathPocket', 'Pocket does not support shape %s.%s') % (subBase.Label, sub)) if obj.EnableRotation != 'Off': PathLog.warning(translate('PathPocket', 'Face might not be within rotation accessibility limits.')) # Determine final depth as highest value of bottom boundbox of vertical face, # in case of uneven faces on bottom if len(self.vert) > 0: vFinDep = self.vert[0].BoundBox.ZMin for vFace in self.vert: if vFace.BoundBox.ZMin > vFinDep: vFinDep = vFace.BoundBox.ZMin # Determine if vertical faces for a loop: Extract planar loop wire as new horizontal face. self.vertical = PathGeom.combineConnectedShapes(self.vert) # pylint: disable=attribute-defined-outside-init self.vWires = [TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical] # pylint: disable=attribute-defined-outside-init for wire in self.vWires: w = PathGeom.removeDuplicateEdges(wire) face = Part.Face(w) # face.tessellate(0.1) if PathGeom.isRoughly(face.Area, 0): msg = translate('PathPocket', 'Vertical faces do not form a loop - ignoring') PathLog.error(msg) # title = translate("Path", "Face Selection Warning") # self.guiMessage(title, msg, True) else: face.translate(FreeCAD.Vector(0, 0, vFinDep - face.BoundBox.ZMin)) self.horiz.append(face) msg = translate('Path', 'Verify final depth of pocket shaped by vertical faces.') PathLog.warning(msg) # title = translate('Path', 'Depth Warning') # self.guiMessage(title, msg, False) # add faces for extensions self.exts = [] # pylint: disable=attribute-defined-outside-init for ext in self.getExtensions(obj): wire = ext.getWire() if wire: face = Part.Face(wire) self.horiz.append(face) self.exts.append(face) # move all horizontal faces to FinalDepth # for f in self.horiz: # f.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - f.BoundBox.ZMin)) # check all faces and see if they are touching/overlapping and combine those into a compound self.horizontal = [] # pylint: disable=attribute-defined-outside-init for shape in PathGeom.combineConnectedShapes(self.horiz): shape.sewShape() # shape.tessellate(0.1) shpZMin = shape.BoundBox.ZMin PathLog.debug('PathGeom.combineConnectedShapes shape.BoundBox.ZMin: {}'.format(shape.BoundBox.ZMin)) if obj.UseOutline: wire = TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) wFace = Part.Face(wire) if wFace.BoundBox.ZMin != shpZMin: wFace.translate(FreeCAD.Vector(0, 0, shpZMin - wFace.BoundBox.ZMin)) self.horizontal.append(wFace) PathLog.debug('PathGeom.combineConnectedShapes shape.BoundBox.ZMin: {}'.format(wFace.BoundBox.ZMin)) else: self.horizontal.append(shape) # extrude all faces up to StartDepth and those are the removal shapes sD = obj.StartDepth.Value fD = obj.FinalDepth.Value clrnc = 0.5 for face in self.horizontal: afD = fD useAngle = angle shpZMin = face.BoundBox.ZMin PathLog.debug('self.horizontal shpZMin: {}'.format(shpZMin)) if self.isFaceUp(subBase, face) is False: useAngle += 180.0 invZ = (-2 * shpZMin) - clrnc face.translate(FreeCAD.Vector(0.0, 0.0, invZ)) shpZMin = -1 * shpZMin else: face.translate(FreeCAD.Vector(0.0, 0.0, -1 * clrnc)) if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off': if shpZMin > obj.FinalDepth.Value: afD = shpZMin if sD <= afD: sD = afD + 1.0 msg = translate('PathPocketShape', 'Start Depth is lower than face depth. Setting to ') PathLog.warning(msg + ' {} mm.'.format(sD)) else: face.translate(FreeCAD.Vector(0, 0, obj.FinalDepth.Value - shpZMin)) extent = FreeCAD.Vector(0, 0, sD - afD + clrnc) extShp = face.removeSplitter().extrude(extent) self.removalshapes.append((extShp, False, 'pathPocketShape', useAngle, axis, sD, afD)) PathLog.debug("Extent values are strDep: {}, finDep: {}, extrd: {}".format(sD, afD, extent)) # Efor face # Efor else: # process the job base object as a whole PathLog.debug(translate("Path", 'Processing model as a whole ...')) finDep = obj.FinalDepth.Value strDep = obj.StartDepth.Value self.outlines = [Part.Face(TechDraw.findShapeOutline(base.Shape, 1, FreeCAD.Vector(0, 0, 1))) for base in self.model] # pylint: disable=attribute-defined-outside-init stockBB = self.stock.Shape.BoundBox self.removalshapes = [] # pylint: disable=attribute-defined-outside-init self.bodies = [] # pylint: disable=attribute-defined-outside-init for outline in self.outlines: outline.translate(FreeCAD.Vector(0, 0, stockBB.ZMin - 1)) body = outline.extrude(FreeCAD.Vector(0, 0, stockBB.ZLength + 2)) self.bodies.append(body) self.removalshapes.append((self.stock.Shape.cut(body), False, 'pathPocketShape', 0.0, 'X', strDep, finDep)) for (shape, hole, sub, angle, axis, strDep, finDep) in self.removalshapes: # pylint: disable=unused-variable shape.tessellate(0.05) # originally 0.1 if self.removalshapes: obj.removalshape = self.removalshapes[0][0] return self.removalshapes
def CreateFromTemplate(job, template): if template.get('version') and 1 == int(template['version']): stockType = template.get('create') if stockType: placement = None posX = template.get('posX') posY = template.get('posY') posZ = template.get('posZ') rotX = template.get('rotX') rotY = template.get('rotY') rotZ = template.get('rotZ') rotW = template.get('rotW') if posX is not None and posY is not None and posZ is not None and rotX is not None and rotY is not None and rotZ is not None and rotW is not None: pos = FreeCAD.Vector(float(posX), float(posY), float(posZ)) rot = FreeCAD.Rotation(float(rotX), float(rotY), float(rotZ), float(rotW)) placement = FreeCAD.Placement(pos, rot) elif posX is not None or posY is not None or posZ is not None or rotX is not None or rotY is not None or rotZ is not None or rotW is not None: PathLog.warning(translate('PathStock', 'Corrupted or incomplete placement information in template - ignoring')) if stockType == StockType.FromBase: xneg = template.get('xneg') xpos = template.get('xpos') yneg = template.get('yneg') ypos = template.get('ypos') zneg = template.get('zneg') zpos = template.get('zpos') neg = None pos = None if xneg is not None and xpos is not None and yneg is not None and ypos is not None and zneg is not None and zpos is not None: neg = FreeCAD.Vector(FreeCAD.Units.Quantity(xneg).Value, FreeCAD.Units.Quantity(yneg).Value, FreeCAD.Units.Quantity(zneg).Value) pos = FreeCAD.Vector(FreeCAD.Units.Quantity(xpos).Value, FreeCAD.Units.Quantity(ypos).Value, FreeCAD.Units.Quantity(zpos).Value) elif xneg is not None or xpos is not None or yneg is not None or ypos is not None or zneg is not None or zpos is not None: PathLog.error(translate('PathStock', 'Corrupted or incomplete specification for creating stock from base - ignoring extent')) return CreateFromBase(job, neg, pos, placement) if stockType == StockType.CreateBox: length = template.get('length') width = template.get('width') height = template.get('height') extent = None if length is not None and width is not None and height is not None: extent = FreeCAD.Vector(FreeCAD.Units.Quantity(length).Value, FreeCAD.Units.Quantity(width).Value, FreeCAD.Units.Quantity(height).Value) elif length is not None or width is not None or height is not None: PathLog.error(translate('PathStock', 'Corrupted or incomplete size for creating a stock box - ignoring size')) return CreateBox(job, extent, placement) if stockType == StockType.CreateCylinder: radius = template.get('radius') height = template.get('height') if radius is not None and height is not None: pass elif radius is not None or height is not None: radius = None height = None PathLog.error(translate('PathStock', 'Corrupted or incomplete size for creating a stock cylinder - ignoring size')) return CreateCylinder(job, radius, height, placement) PathLog.error(translate('PathStock', 'Unsupported stock type named {}').format(stockType)) else: PathLog.error(translate('PathStock', 'Unsupported PathStock template version {}').format(template.get('version'))) return None
def opExecute(self, obj): '''opExecute(obj) ... processes all Base features and Locations and collects them in a list of positions and radii which is then passed to circularHoleExecute(obj, holes). If no Base geometries and no Locations are present, the job's Base is inspected and all drillable features are added to Base. In this case appropriate values for depths are also calculated and assigned. Do not overwrite, implement circularHoleExecute(obj, holes) instead.''' PathLog.track() holes = [] baseSubsTuples = [] subCount = 0 allTuples = [] self.cloneNames = [] # pylint: disable=attribute-defined-outside-init self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init self.rotateFlag = False # pylint: disable=attribute-defined-outside-init self.useTempJobClones('Delete') # pylint: disable=attribute-defined-outside-init self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init self.clearHeight = obj.ClearanceHeight.Value # pylint: disable=attribute-defined-outside-init self.safeHeight = obj.SafeHeight.Value # pylint: disable=attribute-defined-outside-init self.axialFeed = 0.0 # pylint: disable=attribute-defined-outside-init self.axialRapid = 0.0 # pylint: disable=attribute-defined-outside-init def haveLocations(self, obj): if PathOp.FeatureLocations & self.opFeatures(obj): return len(obj.Locations) != 0 return False if obj.EnableRotation == 'Off': strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value else: # Calculate operation heights based upon rotation radii opHeights = self.opDetermineRotationRadii(obj) (self.xRotRad, self.yRotRad, self.zRotRad) = opHeights[0] # pylint: disable=attribute-defined-outside-init (clrOfset, safOfst) = opHeights[1] PathLog.debug("Exec. opHeights[0]: " + str(opHeights[0])) PathLog.debug("Exec. opHeights[1]: " + str(opHeights[1])) # Set clearance and safe heights based upon rotation radii if obj.EnableRotation == 'A(x)': strDep = self.xRotRad elif obj.EnableRotation == 'B(y)': strDep = self.yRotRad else: strDep = max(self.xRotRad, self.yRotRad) finDep = -1 * strDep obj.ClearanceHeight.Value = strDep + clrOfset obj.SafeHeight.Value = strDep + safOfst # Create visual axes when debugging. if PathLog.getLevel(PathLog.thisModule()) == 4: self.visualAxis() # Set axial feed rates based upon horizontal feed rates safeCircum = 2 * math.pi * obj.SafeHeight.Value self.axialFeed = 360 / safeCircum * self.horizFeed # pylint: disable=attribute-defined-outside-init self.axialRapid = 360 / safeCircum * self.horizRapid # pylint: disable=attribute-defined-outside-init # Complete rotational analysis and temp clone creation as needed if obj.EnableRotation == 'Off': PathLog.debug("Enable Rotation setting is 'Off' for {}.".format( obj.Name)) stock = PathUtils.findParentJob(obj).Stock for (base, subList) in obj.Base: baseSubsTuples.append((base, subList, 0.0, 'A', stock)) else: for p in range(0, len(obj.Base)): (bst, at) = self.process_base_geometry_with_rotation( obj, p, subCount) allTuples.extend(at) baseSubsTuples.extend(bst) for base, subs, angle, axis, stock in baseSubsTuples: # rotate shorter angle in opposite direction if angle > 180: angle -= 360 elif angle < -180: angle += 360 # Re-analyze rotated model for drillable holes if obj.EnableRotation != 'Off': rotated_features = self.findHoles(obj, base) for sub in subs: PathLog.debug('sub, angle, axis: {}, {}, {}'.format( sub, angle, axis)) if self.isHoleEnabled(obj, base, sub): pos = self.holePosition(obj, base, sub) if pos: # Identify face to which edge belongs sub_shape = base.Shape.getElement(sub) # Default is to treat selection as 'Face' shape holeBtm = sub_shape.BoundBox.ZMin if obj.EnableRotation != 'Off': # Update Start and Final depths due to rotation, if auto defaults are active parent_face = self._find_parent_face_of_edge( rotated_features, sub_shape) if parent_face: PathLog.debug('parent_face found') holeBtm = parent_face.BoundBox.ZMin if obj.OpStartDepth == obj.StartDepth: obj.StartDepth.Value = parent_face.BoundBox.ZMax PathLog.debug('new StartDepth: {}'.format( obj.StartDepth.Value)) if obj.OpFinalDepth == obj.FinalDepth: obj.FinalDepth.Value = holeBtm PathLog.debug( 'new FinalDepth: {}'.format(holeBtm)) else: PathLog.debug('NO parent_face identified') if base.Shape.getElement(sub).ShapeType == 'Edge': msg = translate( "Path", "Verify Final Depth of holes based on edges. {} depth is: {} mm" .format(sub, round(holeBtm, 4))) + " " msg += translate( "Path", "Always select the bottom edge of the hole when using an edge." ) PathLog.warning(msg) # Warn user if Final Depth set lower than bottom of hole if obj.FinalDepth.Value < holeBtm: msg = translate( "Path", "Final Depth setting is below the hole bottom for {}." .format(sub)) + ' ' msg += translate( "Path", "{} depth is calculated at {} mm".format( sub, round(holeBtm, 4))) PathLog.warning(msg) holes.append({ 'x': pos.x, 'y': pos.y, 'r': self.holeDiameter(obj, base, sub), 'angle': angle, 'axis': axis, 'trgtDep': obj.FinalDepth.Value, 'stkTop': stock.Shape.BoundBox.ZMax }) # haveLocations are populated from user-provided (x, y) coordinates # provided by the user in the Base Locations tab of the Task Editor window if haveLocations(self, obj): for location in obj.Locations: # holes.append({'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'holeBtm': obj.FinalDepth.Value}) holes.append({ 'x': location.x, 'y': location.y, 'r': 0, 'angle': 0.0, 'axis': 'X', 'trgtDep': obj.FinalDepth.Value, 'stkTop': PathUtils.findParentJob(obj).Stock.Shape.BoundBox.ZMax }) if len(holes) > 0: self.circularHoleExecute( obj, holes) # circularHoleExecute() located in PathDrilling.py self.useTempJobClones( 'Delete') # Delete temp job clone group and contents self.guiMessage('title', None, show=True) # Process GUI messages to user PathLog.debug("obj.Name: " + str(obj.Name))
def generateRamps(self, allowBounce=True): edges = self.wire.Edges outedges = [] for edge in edges: israpid = False for redge in self.rapids: if PathGeom.edgesMatch(edge, redge): israpid = True if not israpid: bb = edge.BoundBox p0 = edge.Vertexes[0].Point p1 = edge.Vertexes[1].Point rampangle = self.angle if bb.XLength < 1e-6 and bb.YLength < 1e-6 and bb.ZLength > 0 and p0.z > p1.z: # check if above ignoreAbove parameter - do not generate ramp if it is newEdge, cont = self.checkIgnoreAbove(edge) if newEdge is not None: outedges.append(newEdge) p0.z = self.ignoreAbove if cont: continue plungelen = abs(p0.z - p1.z) projectionlen = plungelen * math.tan(math.radians(rampangle)) # length of the forthcoming ramp projected to XY plane PathLog.debug("Found plunge move at X:{} Y:{} From Z:{} to Z{}, length of ramp: {}".format(p0.x, p0.y, p0.z, p1.z, projectionlen)) if self.method == 'RampMethod3': projectionlen = projectionlen / 2 # next need to determine how many edges in the path after # plunge are needed to cover the length: covered = False coveredlen = 0 rampedges = [] i = edges.index(edge) + 1 while not covered: candidate = edges[i] cp0 = candidate.Vertexes[0].Point cp1 = candidate.Vertexes[1].Point if abs(cp0.z - cp1.z) > 1e-6: # this edge is not parallel to XY plane, not qualified for ramping. break # PathLog.debug("Next edge length {}".format(candidate.Length)) rampedges.append(candidate) coveredlen = coveredlen + candidate.Length if coveredlen > projectionlen: covered = True i = i + 1 if i >= len(edges): break if len(rampedges) == 0: PathLog.debug("No suitable edges for ramping, plunge will remain as such") outedges.append(edge) else: if not covered: if (not allowBounce) or self.method == 'RampMethod2': l = 0 for redge in rampedges: l = l + redge.Length if self.method == 'RampMethod3': rampangle = math.degrees(math.atan(l / (plungelen / 2))) else: rampangle = math.degrees(math.atan(l / plungelen)) PathLog.warning("Cannot cover with desired angle, tightening angle to: {}".format(rampangle)) # PathLog.debug("Doing ramp to edges: {}".format(rampedges)) if self.method == 'RampMethod1': outedges.extend(self.createRampMethod1(rampedges, p0, projectionlen, rampangle)) elif self.method == 'RampMethod2': outedges.extend(self.createRampMethod2(rampedges, p0, projectionlen, rampangle)) else: # if the ramp cannot be covered with Method3, revert to Method1 # because Method1 support going back-and-forth and thus results in same path as Method3 when # length of the ramp is smaller than needed for single ramp. if (not covered) and allowBounce: projectionlen = projectionlen * 2 outedges.extend(self.createRampMethod1(rampedges, p0, projectionlen, rampangle)) else: outedges.extend(self.createRampMethod3(rampedges, p0, projectionlen, rampangle)) else: outedges.append(edge) else: outedges.append(edge) return outedges
def getPath(self): """getPath() ... Call this method on an instance of the class to generate and return path data for the requested path array.""" if len(self.baseList) == 0: PathLog.error( translate("PathArray", "No base objects for PathArray.")) return None base = self.baseList for b in base: if not b.isDerivedFrom("Path::Feature"): return if not b.Path: return b_tool_controller = toolController(b) if not b_tool_controller: return if b_tool_controller != toolController(base[0]): # this may be important if Job output is split by tool controller PathLog.warning( translate( "PathArray", "Arrays of paths having different tool controllers are handled according to the tool controller of the first path.", )) # build copies output = "" random.seed(self.seed) if self.arrayType == "Linear1D": for i in range(self.copies): pos = FreeCAD.Vector( self.offsetVector.x * (i + 1), self.offsetVector.y * (i + 1), self.offsetVector.z * (i + 1), ) pos = self._calculateJitter(pos) for b in base: pl = FreeCAD.Placement() pl.move(pos) np = Path.Path( [cm.transform(pl) for cm in b.Path.Commands]) output += np.toGCode() elif self.arrayType == "Linear2D": if self.swapDirection: for i in range(self.copiesY + 1): for j in range(self.copiesX + 1): if (i % 2) == 0: pos = FreeCAD.Vector( self.offsetVector.x * j, self.offsetVector.y * i, self.offsetVector.z * i, ) else: pos = FreeCAD.Vector( self.offsetVector.x * (self.copiesX - j), self.offsetVector.y * i, self.offsetVector.z * i, ) pos = self._calculateJitter(pos) for b in base: pl = FreeCAD.Placement() # do not process the index 0,0. It will be processed by the base Paths themselves if not (i == 0 and j == 0): pl.move(pos) np = Path.Path([ cm.transform(pl) for cm in b.Path.Commands ]) output += np.toGCode() else: for i in range(self.copiesX + 1): for j in range(self.copiesY + 1): if (i % 2) == 0: pos = FreeCAD.Vector( self.offsetVector.x * i, self.offsetVector.y * j, self.offsetVector.z * i, ) else: pos = FreeCAD.Vector( self.offsetVector.x * i, self.offsetVector.y * (self.copiesY - j), self.offsetVector.z * i, ) pos = self._calculateJitter(pos) for b in base: pl = FreeCAD.Placement() # do not process the index 0,0. It will be processed by the base Paths themselves if not (i == 0 and j == 0): pl.move(pos) np = Path.Path([ cm.transform(pl) for cm in b.Path.Commands ]) output += np.toGCode() # Eif else: for i in range(self.copies): for b in base: ang = 360 if self.copies > 0: ang = self.angle / self.copies * (1 + i) np = self.rotatePath(b.Path, ang, self.centre) output += np.toGCode() # return output return Path.Path(output)
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... return top face''' # Facing is done either against base objects holeShape = None PathLog.debug('depthparams: {}'.format([i for i in self.depthparams])) if obj.Base: PathLog.debug("obj.Base: {}".format(obj.Base)) faces = [] holes = [] holeEnvs = [] oneBase = [obj.Base[0][0], True] sub0 = getattr(obj.Base[0][0].Shape, obj.Base[0][1][0]) minHeight = sub0.BoundBox.ZMax for b in obj.Base: for sub in b[1]: shape = getattr(b[0].Shape, sub) if isinstance(shape, Part.Face): faces.append(shape) if shape.BoundBox.ZMin < minHeight: minHeight = shape.BoundBox.ZMin # Limit to one model base per operation if oneBase[0] is not b[0]: oneBase[1] = False if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face # Analyze internal closed wires to determine if raised or a recess for wire in shape.Wires[1:]: if obj.ExcludeRaisedAreas: ip = self.isPocket(b[0], shape, wire) if ip is False: holes.append((b[0].Shape, wire)) else: holes.append((b[0].Shape, wire)) else: PathLog.warning('The base subobject, "{0}," is not a face. Ignoring "{0}."'.format(sub)) if obj.ExcludeRaisedAreas and len(holes) > 0: for shape, wire in holes: f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams) holeEnvs.append(env) holeShape = Part.makeCompound(holeEnvs) PathLog.debug("Working on a collection of faces {}".format(faces)) planeshape = Part.makeCompound(faces) # If no base object, do planing of top surface of entire model else: planeshape = Part.makeCompound([base.Shape for base in self.model]) PathLog.debug("Working on a shape {}".format(obj.Label)) # Find the correct shape depending on Boundary shape. PathLog.debug("Boundary Shape: {}".format(obj.BoundaryShape)) bb = planeshape.BoundBox # Apply offset for clearing edges offset = 0 if obj.ClearEdges: offset = self.radius + 0.1 bb.XMin = bb.XMin - offset bb.YMin = bb.YMin - offset bb.XMax = bb.XMax + offset bb.YMax = bb.YMax + offset if obj.BoundaryShape == 'Boundbox': bbperim = Part.makeBox(bb.XLength, bb.YLength, 1, FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin), FreeCAD.Vector(0, 0, 1)) env = PathUtils.getEnvelope(partshape=bbperim, depthparams=self.depthparams) if obj.ExcludeRaisedAreas and oneBase[1]: includedFaces = self.getAllIncludedFaces(oneBase[0], env, faceZ=minHeight) if len(includedFaces) > 0: includedShape = Part.makeCompound(includedFaces) includedEnv = PathUtils.getEnvelope(oneBase[0].Shape, subshape=includedShape, depthparams=self.depthparams) env = env.cut(includedEnv) elif obj.BoundaryShape == 'Stock': stock = PathUtils.findParentJob(obj).Stock.Shape env = stock if obj.ExcludeRaisedAreas and oneBase[1]: includedFaces = self.getAllIncludedFaces(oneBase[0], stock, faceZ=minHeight) if len(includedFaces) > 0: stockEnv = PathUtils.getEnvelope(partshape=stock, depthparams=self.depthparams) includedShape = Part.makeCompound(includedFaces) includedEnv = PathUtils.getEnvelope(oneBase[0].Shape, subshape=includedShape, depthparams=self.depthparams) env = stockEnv.cut(includedEnv) elif obj.BoundaryShape == 'Perimeter': if obj.ClearEdges: psZMin = planeshape.BoundBox.ZMin ofstShape = PathUtils.getOffsetArea(planeshape, self.radius * 1.25, plane=planeshape) ofstShape.translate(FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin)) env = PathUtils.getEnvelope(partshape=ofstShape, depthparams=self.depthparams) else: env = PathUtils.getEnvelope(partshape=planeshape, depthparams=self.depthparams) elif obj.BoundaryShape == 'Face Region': import PathScripts.PathSurfaceSupport as PathSurfaceSupport baseShape = oneBase[0].Shape psZMin = planeshape.BoundBox.ZMin ofstShape = PathUtils.getOffsetArea(planeshape, self.tool.Diameter * 1.1, plane=planeshape) ofstShape.translate(FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin)) custDepthparams = self._customDepthParams(obj, obj.StartDepth.Value + 0.1, obj.FinalDepth.Value - 0.1) # only an envelope ofstShapeEnv = PathUtils.getEnvelope(partshape=ofstShape, depthparams=custDepthparams) env = ofstShapeEnv.cut(baseShape) if holeShape: PathLog.debug("Processing holes and face ...") holeEnv = PathUtils.getEnvelope(partshape=holeShape, depthparams=self.depthparams) newEnv = env.cut(holeEnv) tup = newEnv, False, 'pathMillFace', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value else: PathLog.debug("Processing solid face ...") tup = env, False, 'pathMillFace', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value return [tup]
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.''' PathLog.track() if obj.UseComp: self.commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")")) else: self.commandlist.append(Path.Command("(Uncompensated Tool Path)")) shapes = [] self.profileshape = [] # pylint: disable=attribute-defined-outside-init baseSubsTuples = [] subCount = 0 allTuples = [] if obj.Base: # The user has selected subobjects from the base. Process each. if obj.EnableRotation != 'Off': for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] for sub in subsList: subCount += 1 shape = getattr(base.Shape, sub) if isinstance(shape, Part.Face): rtn = False (norm, surf) = self.getFaceNormAndSurf(shape) (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) if rtn is True: (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) # Verify faces are correctly oriented - InverseAngle might be necessary faceIA = getattr(clnBase.Shape, sub) (norm, surf) = self.getFaceNormAndSurf(faceIA) (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) if abs(praAngle) == 180.0: rtn = False if self.isFaceUp(clnBase, faceIA) is False: PathLog.debug('isFaceUp 1 is False') angle -= 180.0 if rtn is True: PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) if obj.InverseAngle is False: if obj.AttemptInverseAngle is True: (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) else: msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") PathLog.warning(msg) if self.isFaceUp(clnBase, faceIA) is False: PathLog.debug('isFaceUp 2 is False') angle += 180.0 else: PathLog.debug(' isFaceUp') else: PathLog.debug("Face appears to be oriented correctly.") if angle < 0.0: angle += 360.0 tup = clnBase, sub, tag, angle, axis, clnStock else: if self.warnDisabledAxis(obj, axis) is False: PathLog.debug(str(sub) + ": No rotation used") axis = 'X' angle = 0.0 tag = base.Name + '_' + axis + str(angle).replace('.', '_') stock = PathUtils.findParentJob(obj).Stock tup = base, sub, tag, angle, axis, stock allTuples.append(tup) if subCount > 1: msg = translate('Path', "Multiple faces in Base Geometry.") + " " msg += translate('Path', "Depth settings will be applied to all faces.") PathLog.warning(msg) (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) subList = [] for o in range(0, len(Tags)): subList = [] for (base, sub, tag, angle, axis, stock) in Grps[o]: subList.append(sub) pair = base, subList, angle, axis, stock 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)) # for base in obj.Base: finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 for (base, subsList, angle, axis, stock) in baseSubsTuples: holes = [] faces = [] faceDepths = [] startDepths = [] for sub in subsList: shape = getattr(base.Shape, sub) if isinstance(shape, Part.Face): faces.append(shape) if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face for wire in shape.Wires[1:]: holes.append((base.Shape, wire)) # Add face depth to list faceDepths.append(shape.BoundBox.ZMin) else: ignoreSub = base.Name + '.' + sub msg = translate('Path', "Found a selected object which is not a face. Ignoring: {}".format(ignoreSub)) PathLog.error(msg) FreeCAD.Console.PrintWarning(msg) # Set initial Start and Final Depths and recalculate depthparams finDep = obj.FinalDepth.Value strDep = obj.StartDepth.Value if strDep > stock.Shape.BoundBox.ZMax: strDep = stock.Shape.BoundBox.ZMax 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) if len(faces) > 0: profileshape = Part.makeCompound(faces) self.profileshape.append(profileshape) if obj.processPerimeter: if obj.HandleMultipleFeatures == 'Collectively': custDepthparams = self.depthparams 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 try: # env = PathUtils.getEnvelope(base.Shape, subshape=profileshape, depthparams=envDepthparams) env = PathUtils.getEnvelope(profileshape, depthparams=envDepthparams) except Exception: # pylint: disable=broad-except # PathUtils.getEnvelope() failed to return an object. PathLog.error(translate('Path', 'Unable to create path for face(s).')) else: tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep shapes.append(tup) elif obj.HandleMultipleFeatures == 'Individually': for shape in faces: # profShape = Part.makeCompound([shape]) 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(base.Shape, subshape=profShape, depthparams=custDepthparams) env = PathUtils.getEnvelope(shape, depthparams=custDepthparams) tup = env, False, 'pathProfileFaces', angle, axis, strDep, finalDep shapes.append(tup) # Lower high Start Depth to top of Stock startDepth = max(startDepths) if obj.StartDepth.Value > startDepth: obj.StartDepth.Value = startDepth else: # Try to build targets from the job base if 1 == len(self.model): if hasattr(self.model[0], "Proxy"): PathLog.debug("hasattr() Proxy") if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet if obj.processCircles or obj.processHoles: for shape in self.model[0].Proxy.getHoles(self.model[0], transform=True): for wire in shape.Wires: drillable = PathUtils.isDrillable(self.model[0].Proxy, wire) 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 shapes.append(tup) if obj.processPerimeter: for shape in self.model[0].Proxy.getOutlines(self.model[0], transform=True): 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 shapes.append(tup) self.removalshapes = shapes # pylint: disable=attribute-defined-outside-init PathLog.debug("%d shapes" % len(shapes)) return shapes
def isDrillable(obj, candidate, tooldiameter=None, includePartials=False): """ Checks candidates to see if they can be drilled. Candidates can be either faces - circular or cylindrical or circular edges. The tooldiameter can be optionally passed. if passed, the check will return False for any holes smaller than the tooldiameter. obj=Shape candidate = Face or Edge tooldiameter=float """ PathLog.track('obj: {} candidate: {} tooldiameter {}'.format( obj, candidate, tooldiameter)) drillable = False try: if candidate.ShapeType == 'Face': face = candidate # eliminate flat faces if (round(face.ParameterRange[0], 8) == 0.0) and (round( face.ParameterRange[1], 8) == round(math.pi * 2, 8)): for edge in face.Edges: # Find seam edge and check if aligned to Z axis. if (isinstance(edge.Curve, Part.Line)): PathLog.debug("candidate is a circle") v0 = edge.Vertexes[0].Point v1 = edge.Vertexes[1].Point #check if the cylinder seam is vertically aligned. Eliminate tilted holes if (numpy.isclose(v1.sub(v0).x, 0, rtol=1e-05, atol=1e-06)) and \ (numpy.isclose(v1.sub(v0).y, 0, rtol=1e-05, atol=1e-06)): drillable = True # vector of top center lsp = Vector(face.BoundBox.Center.x, face.BoundBox.Center.y, face.BoundBox.ZMax) # vector of bottom center lep = Vector(face.BoundBox.Center.x, face.BoundBox.Center.y, face.BoundBox.ZMin) # check if the cylindrical 'lids' are inside the base # object. This eliminates extruded circles but allows # actual holes. if obj.isInside(lsp, 1e-6, False) or obj.isInside( lep, 1e-6, False): PathLog.track( "inside check failed. lsp: {} lep: {}". format(lsp, lep)) drillable = False # eliminate elliptical holes elif not hasattr(face.Surface, "Radius"): PathLog.debug( "candidate face has no radius attribute") drillable = False else: if tooldiameter is not None: drillable = face.Surface.Radius >= tooldiameter / 2 else: drillable = True elif type(face.Surface) == Part.Plane and PathGeom.pointsCoincide( face.Surface.Axis, FreeCAD.Vector(0, 0, 1)): if len(face.Edges) == 1 and type( face.Edges[0].Curve) == Part.Circle: center = face.Edges[0].Curve.Center if obj.isInside(center, 1e-6, False): if tooldiameter is not None: drillable = face.Edges[ 0].Curve.Radius >= tooldiameter / 2 else: drillable = True else: for edge in candidate.Edges: if isinstance(edge.Curve, Part.Circle) and (includePartials or edge.isClosed()): PathLog.debug("candidate is a circle or ellipse") if not hasattr(edge.Curve, "Radius"): PathLog.debug("No radius. Ellipse.") drillable = False else: PathLog.debug("Has Radius, Circle") if tooldiameter is not None: drillable = edge.Curve.Radius >= tooldiameter / 2 if not drillable: FreeCAD.Console.PrintMessage( "Found a drillable hole with diameter: {}: " "too small for the current tool with " "diameter: {}".format( edge.Curve.Radius * 2, tooldiameter)) else: drillable = True PathLog.debug("candidate is drillable: {}".format(drillable)) except Exception as ex: PathLog.warning( translate("PathUtils", "Issue determine drillability: {}").format(ex)) return drillable
def CreateFromTemplate(job, template): if template.get("version") and 1 == int(template["version"]): stockType = template.get("create") if stockType: placement = None posX = template.get("posX") posY = template.get("posY") posZ = template.get("posZ") rotX = template.get("rotX") rotY = template.get("rotY") rotZ = template.get("rotZ") rotW = template.get("rotW") if (posX is not None and posY is not None and posZ is not None and rotX is not None and rotY is not None and rotZ is not None and rotW is not None): pos = FreeCAD.Vector(float(posX), float(posY), float(posZ)) rot = FreeCAD.Rotation(float(rotX), float(rotY), float(rotZ), float(rotW)) placement = FreeCAD.Placement(pos, rot) elif (posX is not None or posY is not None or posZ is not None or rotX is not None or rotY is not None or rotZ is not None or rotW is not None): PathLog.warning( "Corrupted or incomplete placement information in template - ignoring" ) if stockType == StockType.FromBase: xneg = template.get("xneg") xpos = template.get("xpos") yneg = template.get("yneg") ypos = template.get("ypos") zneg = template.get("zneg") zpos = template.get("zpos") neg = None pos = None if (xneg is not None and xpos is not None and yneg is not None and ypos is not None and zneg is not None and zpos is not None): neg = FreeCAD.Vector( FreeCAD.Units.Quantity(xneg).Value, FreeCAD.Units.Quantity(yneg).Value, FreeCAD.Units.Quantity(zneg).Value, ) pos = FreeCAD.Vector( FreeCAD.Units.Quantity(xpos).Value, FreeCAD.Units.Quantity(ypos).Value, FreeCAD.Units.Quantity(zpos).Value, ) elif (xneg is not None or xpos is not None or yneg is not None or ypos is not None or zneg is not None or zpos is not None): PathLog.error( "Corrupted or incomplete specification for creating stock from base - ignoring extent" ) return CreateFromBase(job, neg, pos, placement) if stockType == StockType.CreateBox: PathLog.track(" create box") length = template.get("length") width = template.get("width") height = template.get("height") extent = None if length is not None and width is not None and height is not None: PathLog.track(" have extent") extent = FreeCAD.Vector( FreeCAD.Units.Quantity(length).Value, FreeCAD.Units.Quantity(width).Value, FreeCAD.Units.Quantity(height).Value, ) elif length is not None or width is not None or height is not None: PathLog.error( "Corrupted or incomplete size for creating a stock box - ignoring size" ) else: PathLog.track( " take placement (%s) and extent (%s) from model" % (placement, extent)) return CreateBox(job, extent, placement) if stockType == StockType.CreateCylinder: radius = template.get("radius") height = template.get("height") if radius is not None and height is not None: pass elif radius is not None or height is not None: radius = None height = None PathLog.error( "Corrupted or incomplete size for creating a stock cylinder - ignoring size" ) return CreateCylinder(job, radius, height, placement) PathLog.error( translate("PathStock", "Unsupported stock type named {}").format(stockType)) else: PathLog.error( translate("PathStock", "Unsupported PathStock template version {}").format( template.get("version"))) return None