def setFields(self): self.form.startDepth.setText(FreeCAD.Units.Quantity(self.obj.StartDepth.Value, FreeCAD.Units.Length).UserString) self.form.finalDepth.setText(FreeCAD.Units.Quantity(self.obj.FinalDepth.Value, FreeCAD.Units.Length).UserString) self.form.finishDepth.setText(FreeCAD.Units.Quantity(self.obj.FinishDepth.Value, FreeCAD.Units.Length).UserString) self.form.stepDown.setText(FreeCAD.Units.Quantity(self.obj.StepDown, FreeCAD.Units.Length).UserString) self.form.safeHeight.setText(FreeCAD.Units.Quantity(self.obj.SafeHeight.Value, FreeCAD.Units.Length).UserString) self.form.clearanceHeight.setText(FreeCAD.Units.Quantity(self.obj.ClearanceHeight.Value, FreeCAD.Units.Length).UserString) controllers = PathUtils.getToolControllers(self.obj) labels = [c.Label for c in controllers] self.form.uiToolController.blockSignals(True) self.form.uiToolController.addItems(labels) self.form.uiToolController.blockSignals(False) if self.obj.ToolController is not None: index = self.form.uiToolController.findText( self.obj.ToolController.Label, QtCore.Qt.MatchFixedString) PathLog.debug("searching for TC label {}. Found Index: {}".format(self.obj.ToolController.Label, index)) if index >= 0: self.form.uiToolController.blockSignals(True) self.form.uiToolController.setCurrentIndex(index) self.form.uiToolController.blockSignals(False) else: self.obj.ToolController = PathUtils.findToolController(self.obj) for i in self.obj.Base: self.form.baseList.addItem(i[0].Name) index = self.form.algorithmSelect.findText( self.obj.Algorithm, QtCore.Qt.MatchFixedString) if index >= 0: self.form.algorithmSelect.blockSignals(True) self.form.algorithmSelect.setCurrentIndex(index) self.form.algorithmSelect.blockSignals(False)
def inOutBoneCommands(self, bone, boneAngle, fixedLength): corner = bone.corner(self.toolRadius) bone.tip = bone.inChord.End # in case there is no bone PathLog.debug("corner = (%.2f, %.2f)" % (corner.x, corner.y)) # debugMarker(corner, 'corner', (1., 0., 1.), self.toolRadius) length = fixedLength if bone.obj.Incision == Incision.Custom: length = bone.obj.Custom if bone.obj.Incision == Incision.Adaptive: length = bone.adaptiveLength(boneAngle, self.toolRadius) if length == 0: PathLog.info("no bone after all ..") return [bone.lastCommand, bone.outChord.g1Command(bone.F)] boneInChord = bone.inChord.move(length, boneAngle) boneOutChord = boneInChord.moveTo(bone.outChord.Start) # debugCircle(boneInChord.Start, self.toolRadius, 'boneStart') # debugCircle(boneInChord.End, self.toolRadius, 'boneEnd') bone.tip = boneInChord.End if bone.smooth == 0: return [bone.lastCommand, boneInChord.g1Command(bone.F), boneOutChord.g1Command(bone.F), bone.outChord.g1Command(bone.F)] # reconstruct the corner and convert to an edge offset = corner - bone.inChord.End iChord = Chord(bone.inChord.Start + offset, bone.inChord.End + offset) oChord = Chord(bone.outChord.Start + offset, bone.outChord.End + offset) iLine = iChord.asLine() oLine = oChord.asLine() cornerShape = Part.Shape([iLine, oLine]) # construct a shape representing the cut made by the bone vt0 = FreeCAD.Vector(0, self.toolRadius, 0) vt1 = FreeCAD.Vector(length, self.toolRadius, 0) vb0 = FreeCAD.Vector(0, -self.toolRadius, 0) vb1 = FreeCAD.Vector(length, -self.toolRadius, 0) vm2 = FreeCAD.Vector(length + self.toolRadius, 0, 0) boneBot = Part.LineSegment(vb1, vb0) boneLid = Part.LineSegment(vb0, vt0) boneTop = Part.LineSegment(vt0, vt1) # what we actually want is an Arc - but findIntersect only returns the coincident if one exists # which really sucks because that's the one we're probably not interested in .... boneArc = Part.Arc(vt1, vm2, vb1) # boneArc = Part.Circle(FreeCAD.Vector(length, 0, 0), FreeCAD.Vector(0,0,1), self.toolRadius) boneWire = Part.Shape([boneTop, boneArc, boneBot, boneLid]) boneWire.rotate(FreeCAD.Vector(0, 0, 0), FreeCAD.Vector(0, 0, 1), boneAngle * 180 / math.pi) boneWire.translate(bone.inChord.End) self.boneShapes = [cornerShape, boneWire] bone.inCommands = self.smoothChordCommands(bone, bone.inChord, boneInChord, Part.Edge(iLine), boneWire, corner, bone.smooth & Smooth.In, (1., 0., 0.)) bone.outCommands = self.smoothChordCommands(bone, boneOutChord, bone.outChord, Part.Edge(oLine), boneWire, corner, bone.smooth & Smooth.Out, (0., 1., 0.)) return bone.inCommands + bone.outCommands
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 areaOpShapes(self, obj): '''areaOpShapes(obj) ... return top face''' # Facing is done either against base objects if obj.Base: PathLog.debug("obj.Base: {}".format(obj.Base)) faces = [] for b in obj.Base: for sub in b[1]: shape = getattr(b[0].Shape, sub) if isinstance(shape, Part.Face): faces.append(shape) else: PathLog.debug('The base subobject is not a face') return planeshape = Part.makeCompound(faces) PathLog.debug("Working on a collection of faces {}".format(faces)) # If no base object, do planing of top surface of entire model else: planeshape = self.baseobject.Shape PathLog.debug("Working on a shape {}".format(self.baseobject.Name)) # Find the correct shape depending on Boundary shape. PathLog.debug("Boundary Shape: {}".format(obj.BoundaryShape)) bb = planeshape.BoundBox 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) elif obj.BoundaryShape == 'Stock': stock = PathUtils.findParentJob(obj).Stock.Shape env = stock else: env = PathUtils.getEnvelope(partshape=planeshape, depthparams=self.depthparams) return [(env, False)]
def rotateSel(sel, n): p = sel.Object.Placement loc = sel.Object.Placement.Base r = axis.cross(n) # rotation axis a = DraftVecUtils.angle(n, axis, r) * 180 / math.pi PathLog.debug("oh boy: (%.2f, %.2f, %.2f) -> %.2f" % (r.x, r.y, r.z, a)) Draft.rotate(sel.Object, a, axis=r)
def assignTemplate(self, obj, template): '''assignTemplate(obj, template) ... extract the properties from the given template file and assign to receiver. This will also create any TCs stored in the template.''' tcs = [] if template: tree = xml.parse(template) for job in tree.getroot().iter(JobTemplate.Job): if job.get(JobTemplate.GeometryTolerance): obj.GeometryTolerance = float(job.get(JobTemplate.GeometryTolerance)) if job.get(JobTemplate.PostProcessor): obj.PostProcessor = job.get(JobTemplate.PostProcessor) if job.get(JobTemplate.PostProcessorArgs): obj.PostProcessorArgs = job.get(JobTemplate.PostProcessorArgs) else: obj.PostProcessorArgs = '' if job.get(JobTemplate.PostProcessorOutputFile): obj.PostProcessorOutputFile = job.get(JobTemplate.PostProcessorOutputFile) if job.get(JobTemplate.Description): obj.Description = job.get(JobTemplate.Description) for tc in tree.getroot().iter(JobTemplate.ToolController): tcs.append(PathToolController.FromTemplate(tc)) for stock in tree.getroot().iter(JobTemplate.Stock): obj.Stock = PathStock.CreateFromTemplate(self, stock) else: tcs.append(PathToolController.Create()) PathLog.debug("setting tool controllers (%d)" % len(tcs)) obj.ToolController = tcs
def updateSelection(self): sel = FreeCADGui.Selection.getSelectionEx() PathLog.track(len(sel)) if len(sel) == 1 and len(sel[0].SubObjects) == 1: if 'Vertex' == sel[0].SubObjects[0].ShapeType: self.form.orientGroup.setEnabled(False) self.form.setOrigin.setEnabled(True) self.form.moveToOrigin.setEnabled(True) else: self.form.orientGroup.setEnabled(True) self.form.setOrigin.setEnabled(False) self.form.moveToOrigin.setEnabled(False) else: self.form.orientGroup.setEnabled(False) self.form.setOrigin.setEnabled(False) self.form.moveToOrigin.setEnabled(False) if len(sel) == 1 and sel[0].Object == self.obj.Base: self.form.centerInStock.setEnabled(True) self.form.centerInStockXY.setEnabled(True) else: if len(sel) == 1 and self.obj.Base: PathLog.debug("sel = %s / %s" % (sel[0].Object.Label, self.obj.Base.Label)) else: PathLog.debug("sel len = %d" % len(sel)) self.form.centerInStock.setEnabled(False) self.form.centerInStockXY.setEnabled(False)
def orderAndFlipEdges(self, inputEdges): PathLog.track("entry(%.2f, %.2f, %.2f), exit(%.2f, %.2f, %.2f)" % (self.entry.x, self.entry.y, self.entry.z, self.exit.x, self.exit.y, self.exit.z)) self.edgesOrder = [] outputEdges = [] p0 = self.entry lastP = p0 edges = copy.copy(inputEdges) while edges: # print("(%.2f, %.2f, %.2f) %d %d" % (p0.x, p0.y, p0.z)) for e in copy.copy(edges): p1 = e.valueAt(e.FirstParameter) p2 = e.valueAt(e.LastParameter) if PathGeom.pointsCoincide(p1, p0): outputEdges.append((e, False)) edges.remove(e) lastP = None p0 = p2 debugEdge(e, ">>>>> no flip") break elif PathGeom.pointsCoincide(p2, p0): flipped = PathGeom.flipEdge(e) if not flipped is None: outputEdges.append((flipped, True)) else: p0 = None cnt = 0 for p in reversed(e.discretize(Deflection=0.01)): if not p0 is None: outputEdges.append((Part.Edge(Part.LineSegment(p0, p)), True)) cnt = cnt + 1 p0 = p PathLog.info("replaced edge with %d straight segments" % cnt) edges.remove(e) lastP = None p0 = p1 debugEdge(e, ">>>>> flip") break else: debugEdge(e, "<<<<< (%.2f, %.2f, %.2f)" % (p0.x, p0.y, p0.z)) if lastP == p0: self.edgesOrder.append(outputEdges) self.edgesOrder.append(edges) print('input edges:') for e in inputEdges: debugEdge(e, ' ', False) print('ordered edges:') for e, flip in outputEdges: debugEdge(e, ' %c ' % ('<' if flip else '>'), False) print('remaining edges:') for e in edges: debugEdge(e, ' ', False) raise ValueError("No connection to %s" % (p0)) elif lastP: PathLog.debug("xxxxxx (%.2f, %.2f, %.2f) (%.2f, %.2f, %.2f)" % (p0.x, p0.y, p0.z, lastP.x, lastP.y, lastP.z)) else: PathLog.debug("xxxxxx (%.2f, %.2f, %.2f) -" % (p0.x, p0.y, p0.z)) lastP = p0 PathLog.track("-") return outputEdges
def getFields(self): if self.obj: if hasattr(self.obj, "StartDepth"): self.obj.StartDepth = FreeCAD.Units.Quantity(self.form.startDepth.text()).Value if hasattr(self.obj, "FinalDepth"): self.obj.FinalDepth = FreeCAD.Units.Quantity(self.form.finalDepth.text()).Value if hasattr(self.obj, "SafeHeight"): self.obj.SafeHeight = FreeCAD.Units.Quantity(self.form.safeHeight.text()).Value if hasattr(self.obj, "ClearanceHeight"): self.obj.ClearanceHeight = FreeCAD.Units.Quantity(self.form.clearanceHeight.text()).Value if hasattr(self.obj, "StepDown"): self.obj.StepDown = FreeCAD.Units.Quantity(self.form.stepDown.text()).Value if hasattr(self.obj, "MaterialAllowance"): self.obj.MaterialAllowance = FreeCAD.Units.Quantity(self.form.extraOffset.text()).Value if hasattr(self.obj, "UseStartPoint"): self.obj.UseStartPoint = self.form.useStartPoint.isChecked() if hasattr(self.obj, "Algorithm"): self.obj.Algorithm = str(self.form.algorithmSelect.currentText()) if hasattr(self.obj, "CutMode"): self.obj.CutMode = str(self.form.cutMode.currentText()) if hasattr(self.obj, "UseZigZag"): self.obj.UseZigZag = self.form.useZigZag.isChecked() if hasattr(self.obj, "ZigUnidirectional"): self.obj.ZigUnidirectional = self.form.zigZagUnidirectional.isChecked() if hasattr(self.obj, "ZigZagAngle"): self.obj.ZigZagAngle = FreeCAD.Units.Quantity(self.form.zigZagAngle.text()).Value if hasattr(self.obj, "StepOver"): self.obj.StepOver = self.form.stepOverPercent.value() if hasattr(self.obj, "ToolController"): PathLog.debug("name: {}".format(self.form.uiToolController.currentText())) tc = PathUtils.findToolController(self.obj, self.form.uiToolController.currentText()) self.obj.ToolController = tc self.obj.Proxy.execute(self.obj)
def __init__(self, edge, tag, i, segm, maxZ): debugEdge(edge, 'MapWireToTag(%.2f, %.2f, %.2f)' % (i.x, i.y, i.z)) self.tag = tag self.segm = segm self.maxZ = maxZ if PathGeom.pointsCoincide(edge.valueAt(edge.FirstParameter), i): tail = edge self.commands = [] debugEdge(tail, '.........=') elif PathGeom.pointsCoincide(edge.valueAt(edge.LastParameter), i): debugEdge(edge, '++++++++ .') self.commands = PathGeom.cmdsForEdge(edge, segm=segm) tail = None else: e, tail = PathGeom.splitEdgeAt(edge, i) debugEdge(e, '++++++++ .') self.commands = PathGeom.cmdsForEdge(e, segm=segm) debugEdge(tail, '.........-') self.initialEdge = edge self.tail = tail self.edges = [] self.entry = i if tail: PathLog.debug("MapWireToTag(%s - %s)" % (i, tail.valueAt(tail.FirstParameter))) else: PathLog.debug("MapWireToTag(%s - )" % i) self.complete = False self.haveProblem = False
def getFields(self): if self.obj: if hasattr(self.obj, "StartDepth"): self.obj.StartDepth = FreeCAD.Units.Quantity(self.form.startDepth.text()).Value if hasattr(self.obj, "FinalDepth"): self.obj.FinalDepth = FreeCAD.Units.Quantity(self.form.finalDepth.text()).Value if hasattr(self.obj, "SafeHeight"): self.obj.SafeHeight = FreeCAD.Units.Quantity(self.form.safeHeight.text()).Value if hasattr(self.obj, "ClearanceHeight"): self.obj.ClearanceHeight = FreeCAD.Units.Quantity(self.form.clearanceHeight.text()).Value if hasattr(self.obj, "StepDown"): self.obj.StepDown = FreeCAD.Units.Quantity(self.form.stepDown.text()).Value if hasattr(self.obj, "OffsetExtra"): self.obj.OffsetExtra = FreeCAD.Units.Quantity(self.form.extraOffset.text()).Value if hasattr(self.obj, "RollRadius"): self.obj.RollRadius = FreeCAD.Units.Quantity(self.form.rollRadius.text()).Value if hasattr(self.obj, "UseComp"): self.obj.UseComp = self.form.useCompensation.isChecked() if hasattr(self.obj, "UseStartPoint"): self.obj.UseStartPoint = self.form.useStartPoint.isChecked() if hasattr(self.obj, "UseEndPoint"): self.obj.UseEndPoint = self.form.useEndPoint.isChecked() if hasattr(self.obj, "Side"): self.obj.Side = str(self.form.cutSide.currentText()) if hasattr(self.obj, "Direction"): self.obj.Direction = str(self.form.direction.currentText()) if hasattr(self.obj, "ToolController"): PathLog.debug("name: {}".format(self.form.uiToolController.currentText())) tc = PathUtils.findToolController(self.obj, self.form.uiToolController.currentText()) self.obj.ToolController = tc self.obj.Proxy.execute(self.obj)
def createTagsPositionDisabled(self, obj, positionsIn, disabledIn): rawTags = [] for i, pos in enumerate(positionsIn): tag = Tag(i, pos.x, pos.y, obj.Width.Value, obj.Height.Value, obj.Angle, obj.Radius, not i in disabledIn) tag.createSolidsAt(self.pathData.minZ, self.toolRadius) rawTags.append(tag) # disable all tags that intersect with their previous tag prev = None tags = [] positions = [] disabled = [] for i, tag in enumerate(self.pathData.sortedTags(rawTags)): if tag.enabled: if prev: if prev.solid.common(tag.solid).Faces: PathLog.info("Tag #%d intersects with previous tag - disabling\n" % i) PathLog.debug("this tag = %d [%s]" % (i, tag.solid.BoundBox)) tag.enabled = False elif self.pathData.edges: e = self.pathData.edges[0] p0 = e.valueAt(e.FirstParameter) p1 = e.valueAt(e.LastParameter) if tag.solid.isInside(p0, PathGeom.Tolerance, True) or tag.solid.isInside(p1, PathGeom.Tolerance, True): PathLog.info("Tag #%d intersects with starting point - disabling\n" % i) tag.enabled = False if tag.enabled: prev = tag PathLog.debug("previousTag = %d [%s]" % (i, prev)) else: disabled.append(i) tag.id = i # assigne final id tags.append(tag) positions.append(tag.originAt(self.pathData.minZ)) return (tags, positions, disabled)
def allow(self, doc, obj, sub): PathLog.debug('obj: {} sub: {}'.format(obj, sub)) if hasattr(obj, "Shape") and sub: shape = obj.Shape subobj = shape.getElement(sub) return PathUtils.isDrillable(shape, subobj, includePartials = True) else: return False
def addToolController(self, tc): group = self.obj.ToolController PathLog.debug("addToolController(%s): %s" % (tc.Label, [t.Label for t in group])) if tc.Name not in [str(t.Name) for t in group]: tc.setExpression('VertRapid', "%s.%s" % (self.setupSheet.expressionReference(), PathSetupSheet.Template.VertRapid)) tc.setExpression('HorizRapid', "%s.%s" % (self.setupSheet.expressionReference(), PathSetupSheet.Template.HorizRapid)) group.append(tc) self.obj.ToolController = group
def onDelete(self, arg1=None, arg2=None): PathLog.debug("Deleting Dressup") '''this makes sure that the base operation is added back to the project and visible''' FreeCADGui.ActiveDocument.getObject(arg1.Object.Base.Name).Visibility = True job = PathUtils.findParentJob(self.obj) job.Proxy.addOperation(arg1.Object.Base, arg1.Object) arg1.Object.Base = None return True
def allow(self, doc, obj, sub): PathLog.debug('obj: {} sub: {}'.format(obj, sub)) if hasattr(obj, "Shape"): obj = obj.Shape subobj = obj.getElement(sub) return PathUtils.isDrillable(obj, subobj) else: return False
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.''' 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 = [] if obj.Base: # The user has selected subobjects from the base. Process each. holes = [] faces = [] 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 numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face holes += shape.Wires[1:] else: FreeCAD.Console.PrintWarning("found a base object which is not a face. Can't continue.") return for wire in holes: f = Part.makeFace(wire, 'Part::FaceMakerSimple') drillable = PathUtils.isDrillable(self.baseobject.Shape, wire) if (drillable and obj.processCircles) or (not drillable and obj.processHoles): env = PathUtils.getEnvelope(self.baseobject.Shape, subshape=f, depthparams=self.depthparams) shapes.append((env, True)) if len(faces) > 0: profileshape = Part.makeCompound(faces) if obj.processPerimeter: env = PathUtils.getEnvelope(self.baseobject.Shape, subshape=profileshape, depthparams=self.depthparams) shapes.append((env, False)) else: # Try to build targets from the job base if hasattr(self.baseobject, "Proxy"): if isinstance(self.baseobject.Proxy, ArchPanel.PanelSheet): # process the sheet if obj.processCircles or obj.processHoles: for shape in self.baseobject.Proxy.getHoles(self.baseobject, transform=True): for wire in shape.Wires: drillable = PathUtils.isDrillable(self.baseobject.Proxy, wire) if (drillable and obj.processCircles) or (not drillable and obj.processHoles): f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope(self.baseobject.Shape, subshape=f, depthparams=self.depthparams) shapes.append((env, True)) if obj.processPerimeter: for shape in self.baseobject.Proxy.getOutlines(self.baseobject, transform=True): for wire in shape.Wires: f = Part.makeFace(wire, 'Part::FaceMakerSimple') env = PathUtils.getEnvelope(self.baseobject.Shape, subshape=f, depthparams=self.depthparams) shapes.append((env, False)) PathLog.debug("%d shapes" % len(shapes)) return shapes
def pointIsOnPath(self, p): v = Part.Vertex(self.pointAtBottom(p)) PathLog.debug("pt = (%f, %f, %f)" % (v.X, v.Y, v.Z)) for e in self.bottomEdges: indent = "{} ".format(e.distToShape(v)[0]) debugEdge(e, indent, True) if PathGeom.isRoughly(0.0, v.distToShape(e)[0], 0.1): return True return False
def updateBase(self): newlist = [] for i in range(self.form.baseList.count()): item = self.form.baseList.item(i) obj = item.data(self.DataObject) sub = str(item.data(self.DataObjectSub)) base = (obj, sub) newlist.append(base) PathLog.debug("Setting new base: %s -> %s" % (self.obj.Base, newlist)) self.obj.Base = newlist
def setupToolType(self, tt): PathLog.track() if 'Undefined' == tt: tt = Path.Tool.getToolTypes(Path.Tool())[0] if tt in self.ToolTypeImage: self.editor = self.ToolTypeImage[tt](self) else: PathLog.debug("weak supported ToolType = %s" % (tt)) self.editor = ToolEditorDefault(self) self.editor.setupUI()
def generateHelix(self): edges = self.wire.Edges minZ = self.findMinZ(edges) outedges = [] i = 0 while i < len(edges): edge = edges[i] 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 if bb.XLength < 1e-6 and bb.YLength < 1e-6 and bb.ZLength > 0 and p0.z > p1.z: # plungelen = abs(p0.z-p1.z) PathLog.debug("Found plunge move at X:{} Y:{} From Z:{} to Z{}, Searching for closed loop".format(p0.x, p0.y, p0.z, p1.z)) # next need to determine how many edges in the path after plunge are needed to cover the length: loopFound = False rampedges = [] j = i + 1 while not loopFound: candidate = edges[j] cp0 = candidate.Vertexes[0].Point cp1 = candidate.Vertexes[1].Point if PathGeom.pointsCoincide(p1, cp1): # found closed loop loopFound = True rampedges.append(candidate) break 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) j = j + 1 if j >= len(edges): break if len(rampedges) == 0 or not loopFound: PathLog.debug("No suitable helix found") outedges.append(edge) else: outedges.extend(self.createHelix(rampedges, p0, p1)) if not PathGeom.isRoughly(p1.z, minZ): # the edges covered by the helix not handled again, # unless reached the bottom height i = j else: outedges.append(edge) else: outedges.append(edge) i = i + 1 return outedges
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 depthSet(self, obj, spinbox, prop): z = self.selectionZLevel(FreeCADGui.Selection.getSelectionEx()) if z is not None: PathLog.debug("depthSet(%s, %s, %.2f)" % (obj.Label, prop, z)) if spinbox.expression(): obj.setExpression(prop, None) self.setDirty() spinbox.updateSpinBox(FreeCAD.Units.Quantity(z, FreeCAD.Units.Length)) if spinbox.updateProperty(): self.setDirty() else: PathLog.info("depthSet(-)")
def updateBase(self): PathLog.track() shapes = [] for i in range(self.form.baseList.count()): item = self.form.baseList.item(i) obj = item.data(self.super().DataObject) sub = item.data(self.super().DataObjectSub) if not sub: shapes.append(obj) PathLog.debug("Setting new base shapes: %s -> %s" % (self.obj.BaseShapes, shapes)) self.obj.BaseShapes = shapes return self.super().updateBase()
def setFields(self): self.form.startDepth.setText(FreeCAD.Units.Quantity(self.obj.StartDepth.Value, FreeCAD.Units.Length).UserString) self.form.finalDepth.setText(FreeCAD.Units.Quantity(self.obj.FinalDepth.Value, FreeCAD.Units.Length).UserString) self.form.safeHeight.setText(FreeCAD.Units.Quantity(self.obj.SafeHeight.Value, FreeCAD.Units.Length).UserString) self.form.clearanceHeight.setText(FreeCAD.Units.Quantity(self.obj.ClearanceHeight.Value, FreeCAD.Units.Length).UserString) self.form.stepDown.setText(FreeCAD.Units.Quantity(self.obj.StepDown.Value, FreeCAD.Units.Length).UserString) self.form.extraOffset.setText(FreeCAD.Units.Quantity(self.obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) self.form.rollRadius.setText(FreeCAD.Units.Quantity(self.obj.RollRadius.Value, FreeCAD.Units.Length).UserString) self.form.useCompensation.setChecked(self.obj.UseComp) self.form.useStartPoint.setChecked(self.obj.UseStartPoint) self.form.useEndPoint.setChecked(self.obj.UseEndPoint) self.form.processHoles.setChecked(self.obj.processHoles) self.form.processPerimeter.setChecked(self.obj.processPerimeter) self.form.processCircles.setChecked(self.obj.processCircles) index = self.form.cutSide.findText( self.obj.Side, QtCore.Qt.MatchFixedString) if index >= 0: self.form.cutSide.blockSignals(True) self.form.cutSide.setCurrentIndex(index) self.form.cutSide.blockSignals(False) index = self.form.direction.findText( self.obj.Direction, QtCore.Qt.MatchFixedString) if index >= 0: self.form.direction.blockSignals(True) self.form.direction.setCurrentIndex(index) self.form.direction.blockSignals(False) controllers = PathUtils.getToolControllers(self.obj) labels = [c.Label for c in controllers] self.form.uiToolController.blockSignals(True) self.form.uiToolController.addItems(labels) self.form.uiToolController.blockSignals(False) if self.obj.ToolController is not None: index = self.form.uiToolController.findText( self.obj.ToolController.Label, QtCore.Qt.MatchFixedString) PathLog.debug("searching for TC label {}. Found Index: {}".format(self.obj.ToolController.Label, index)) if index >= 0: self.form.uiToolController.blockSignals(True) self.form.uiToolController.setCurrentIndex(index) self.form.uiToolController.blockSignals(False) else: self.obj.ToolController = PathUtils.findToolController(self.obj) self.form.baseList.blockSignals(True) for i in self.obj.Base: for sub in i[1]: self.form.baseList.addItem(i[0].Name + "." + sub) self.form.baseList.blockSignals(False) self.form.update()
def updateBase(self): '''updateBase() ... helper function to transfer current table to obj''' PathLog.track() newlist = [] for i in range(self.form.baseList.rowCount()): item = self.form.baseList.item(i, 0) obj = item.data(self.DataObject) sub = str(item.data(self.DataObjectSub)) base = (obj, sub) PathLog.debug("keeping (%s.%s)" % (obj.Label, sub)) newlist.append(base) PathLog.debug("obj.Base=%s newlist=%s" % (self.obj.Base, newlist)) self.obj.Base = newlist
def processTags(self, obj): tagID = 0 if PathLog.getLevel(PathLog.thisModule()) == PathLog.Level.DEBUG: for tag in self.tags: tagID += 1 if tag.enabled: PathLog.debug("x=%s, y=%s, z=%s" % (tag.x, tag.y, self.pathData.minZ)) # debugMarker(FreeCAD.Vector(tag.x, tag.y, self.pathData.minZ), "tag-%02d" % tagID , (1.0, 0.0, 1.0), 0.5) # if tag.angle != 90: # debugCone(tag.originAt(self.pathData.minZ), tag.r1, tag.r2, tag.actualHeight, "tag-%02d" % tagID) # else: # debugCylinder(tag.originAt(self.pathData.minZ), tag.fullWidth()/2, tag.actualHeight, "tag-%02d" % tagID) obj.Path = self.createPath(obj, self.pathData, self.tags)
def adaptiveLength(self, boneAngle, toolRadius): angle = self.angle() distance = self.distance(toolRadius) # there is something weird happening if the boneAngle came from a horizontal/vertical t-bone # for some reason pi/2 is not equal to pi/2 if math.fabs(angle - boneAngle) < 0.00001: # moving directly towards the corner PathLog.debug("adaptive - on target: %.2f - %.2f" % (distance, toolRadius)) return distance - toolRadius PathLog.debug("adaptive - angles: corner=%.2f bone=%.2f diff=%.12f" % (angle/math.pi, boneAngle/math.pi, angle - boneAngle)) # The bones root and end point form a triangle with the intersection of the tool path # with the toolRadius circle around the bone end point. # In case the math looks questionable, look for "triangle ssa" # c = distance # b = self.toolRadius # beta = fabs(boneAngle - angle) beta = math.fabs(addAngle(boneAngle, -angle)) D = (distance / toolRadius) * math.sin(beta) if D > 1: # no intersection PathLog.debug("adaptive - no intersection - no bone") return 0 gamma = math.asin(D) alpha = math.pi - beta - gamma length = toolRadius * math.sin(alpha) / math.sin(beta) if D < 1 and toolRadius < distance: # there exists a second solution beta2 = beta gamma2 = math.pi - gamma alpha2 = math.pi - beta2 - gamma2 length2 = toolRadius * math.sin(alpha2) / math.sin(beta2) length = min(length, length2) PathLog.debug("adaptive corner=%.2f * %.2f˚ -> bone=%.2f * %.2f˚" % (distance, angle, length, boneAngle)) return length
def onDelete(self, obj, arg2=None): '''Called by the view provider, there doesn't seem to be a callback on the obj itself.''' PathLog.track(obj.Label, arg2) doc = obj.Document # the first to tear down are the ops, they depend on other resources PathLog.debug('taking down ops: %s' % [o.Name for o in self.allOperations()]) while obj.Operations.Group: op = obj.Operations.Group[0] if not op.ViewObject or not hasattr(op.ViewObject.Proxy, 'onDelete') or op.ViewObject.Proxy.onDelete(op.ViewObject, ()): doc.removeObject(op.Name) obj.Operations.Group = [] doc.removeObject(obj.Operations.Name) obj.Operations = None # stock could depend on Base if obj.Stock: PathLog.debug('taking down stock') doc.removeObject(obj.Stock.Name) obj.Stock = None # base doesn't depend on anything inside job if obj.Base: PathLog.debug('taking down base') doc.removeObject(obj.Base.Name) obj.Base = None # Tool controllers don't depend on anything PathLog.debug('taking down tool controller') for tc in obj.ToolController: doc.removeObject(tc.Name) obj.ToolController = []
def processEdge(self, index, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict): tagCount = 0 currentLength += edge.Length if edge.Length >= minLength: while lastTagLength + tagDistance < currentLength: tagCount += 1 lastTagLength += tagDistance if tagCount > 0: PathLog.debug(" index=%d -> count=%d" % (index, tagCount)) edgeDict[index] = tagCount else: PathLog.debug(" skipping=%-2d (%.2f)" % (index, edge.Length)) return (currentLength, lastTagLength)
def checkForFacesLoop(self, base, subsList): '''checkForFacesLoop(base, subsList)... Accepts a list of face names for the given base. Checks to determine if they are looped together. ''' PathLog.track() fCnt = 0 go = True vertLoopFace = None tempNameList = [] delTempNameList = 0 saSum = FreeCAD.Vector(0.0, 0.0, 0.0) norm = FreeCAD.Vector(0.0, 0.0, 0.0) surf = FreeCAD.Vector(0.0, 0.0, 0.0) precision = 6 def makeTempExtrusion(base, sub, fCnt): extName = 'tmpExtrude' + str(fCnt) wireName = 'tmpWire' + str(fCnt) wr = Part.Wire(Part.__sortEdges__( base.Shape.getElement(sub).Edges)) if wr.isNull(): PathLog.debug('No wire created from {}'.format(sub)) return (False, 0, 0) else: tmpWire = FreeCAD.ActiveDocument.addObject( 'Part::Feature', wireName).Shape = wr tmpWire = FreeCAD.ActiveDocument.getObject(wireName) tmpExt = FreeCAD.ActiveDocument.addObject( 'Part::Extrusion', extName) tmpExt = FreeCAD.ActiveDocument.getObject(extName) tmpExt.Base = tmpWire tmpExt.DirMode = "Normal" tmpExt.DirLink = None tmpExt.LengthFwd = 10.0 tmpExt.LengthRev = 0.0 tmpExt.Solid = True tmpExt.Reversed = False tmpExt.Symmetric = False tmpExt.TaperAngle = 0.0 tmpExt.TaperAngleRev = 0.0 tmpExt.recompute() tmpExt.purgeTouched() tmpWire.purgeTouched() return (True, tmpWire, tmpExt) def roundValue(precision, val): # Convert VALxe-15 numbers to zero if PathGeom.isRoughly(0.0, val) is True: return 0.0 # Convert VAL.99999999 to next integer elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: return round(val) else: return round(val, precision) # Determine precision from Tolerance for i in range(0, 13): if PathGeom.Tolerance * (i * 10) == 1.0: precision = i break # Sub Surface.Axis values of faces # Vector of (0, 0, 0) will suggests a loop for sub in subsList: if 'Face' in sub: fCnt += 1 saSum = saSum.add(base.Shape.getElement(sub).Surface.Axis) # Minimim of three faces required for loop to exist if fCnt < 3: go = False # Determine if all faces combined point toward loop center = False if PathGeom.isRoughly(0, saSum.x): if PathGeom.isRoughly(0, saSum.y): if PathGeom.isRoughly(0, saSum.z): PathLog.debug( "Combined subs suggest loop of faces. Checking ...") go = True if go is True: lastExtrusion = None matchList = [] go = False # Cycle through subs, extruding to solid for each for sub in subsList: if 'Face' in sub: fCnt += 1 go = False # Extrude face to solid (rtn, tmpWire, tmpExt) = makeTempExtrusion(base, sub, fCnt) # If success, record new temporary objects for deletion if rtn is True: tempNameList.append(tmpExt.Name) tempNameList.append(tmpWire.Name) delTempNameList += 1 if lastExtrusion is None: lastExtrusion = tmpExt rtn = True else: go = False break # Cycle through faces on each extrusion, looking for common normal faces for rotation analysis if len(matchList) == 0: for fc in lastExtrusion.Shape.Faces: (norm, raw) = self.getFaceNormAndSurf(fc) rnded = FreeCAD.Vector( roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z)) if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0: for fc2 in tmpExt.Shape.Faces: (norm2, raw2) = self.getFaceNormAndSurf(fc2) # pylint: disable=unused-variable rnded2 = FreeCAD.Vector( roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z)) if rnded == rnded2: matchList.append(fc2) go = True else: for m in matchList: (norm, raw) = self.getFaceNormAndSurf(m) rnded = FreeCAD.Vector( roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z)) for fc2 in tmpExt.Shape.Faces: (norm2, raw2) = self.getFaceNormAndSurf(fc2) rnded2 = FreeCAD.Vector( roundValue(precision, raw2.x), roundValue(precision, raw2.y), roundValue(precision, raw2.z)) if rnded.x == 0.0 or rnded.y == 0.0 or rnded.z == 0.0: if rnded == rnded2: go = True # Eif if go is False: break # Eif # Eif 'Face' # Efor if go is True: go = False if len(matchList) == 2: saTotal = FreeCAD.Vector(0.0, 0.0, 0.0) for fc in matchList: (norm, raw) = self.getFaceNormAndSurf(fc) rnded = FreeCAD.Vector(roundValue(precision, raw.x), roundValue(precision, raw.y), roundValue(precision, raw.z)) if (rnded.y > 0.0 or rnded.z > 0.0) and vertLoopFace is None: vertLoopFace = fc saTotal = saTotal.add(rnded) if saTotal == FreeCAD.Vector(0.0, 0.0, 0.0): if vertLoopFace is not None: go = True if go is True: (norm, surf) = self.getFaceNormAndSurf(vertLoopFace) else: PathLog.debug(translate('Path', 'Can not identify loop.')) if delTempNameList > 0: for tmpNm in tempNameList: FreeCAD.ActiveDocument.removeObject(tmpNm) return (go, norm, surf)
def getLeadStart(self, obj, queue, action): '''returns Lead In G-code.''' results = [] op = PathDressup.baseOp(obj.Base) tc = PathDressup.toolController(obj.Base) horizFeed = tc.HorizFeed.Value vertFeed = tc.VertFeed.Value toolnummer = tc.ToolNumber arcs_identical = False # Set the correct twist command if self.getDirectionOfPath(obj) == 'left': arcdir = "G3" else: arcdir = "G2" R = obj.Length.Value # Radius of roll or length if queue[1].Name == "G1": # line p0 = queue[0].Placement.Base p1 = queue[1].Placement.Base v = self.normalize(p1.sub(p0)) # PathLog.debug(" CURRENT_IN : P0 Z:{} p1 Z:{}".format(p0.z,p1.z)) else: p0 = queue[0].Placement.Base p1 = queue[1].Placement.Base v = self.normalize(p1.sub(p0)) # PathLog.debug(" CURRENT_IN ARC : P0 X:{} Y:{} P1 X:{} Y:{} ".format(p0.x,p0.y,p1.x,p1.y)) # Calculate offset vector (will be overwritten for arcs) if self.getDirectionOfPath(obj) == 'right': off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0) else: off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0) # Check if we enter at line or arc command if queue[1].Name in movecommands and queue[1].Name not in arccommands: # We have a line move vec = p1.sub(p0) vec_n = self.normalize(vec) vec_inv = self.invert(vec_n) vec_off = self.multiply(vec_inv, obj.ExtendLeadIn) #PathLog.debug("LineCMD: {}, Vxinv: {}, Vyinv: {}, Vxoff: {}, Vyoff: {}".format(queue[0].Name, vec_inv.x, vec_inv.y, vec_off.x, vec_off.y)) else: # We have an arc move # Calculate coordinates for middle of circle pij = copy.deepcopy(p0) pij.x += queue[1].Parameters['I'] pij.y += queue[1].Parameters['J'] # Check if lead in and operation go in same direction (usually for inner circles) if arcdir == queue[1].Name: arcs_identical = True # Calculate vector circle start -> circle middle vec_circ = pij.sub(p0) # Rotate vector to get direction for lead in if arcdir == "G2": vec_rot = self.rotate(vec_circ, 90) else: vec_rot = self.rotate(vec_circ, -90) # Normalize and invert vector vec_n = self.normalize(vec_rot) v = self.invert(vec_n) # Calculate offset of lead in if arcdir == "G3": off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0) else: off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0) # Multiply offset by LeadIn length vec_off = self.multiply(vec_n, obj.ExtendLeadIn) offsetvector = FreeCAD.Vector(v.x*R-vec_off.x, v.y*R-vec_off.y, 0) # IJ if obj.RadiusCenter == 'Radius': leadstart = (p0.add(off_v)).sub(offsetvector) # Rmode if arcs_identical: t = p0.sub(leadstart) t = p0.add(t) leadstart = t offsetvector = self.multiply(offsetvector, -1) else: leadstart = p0.add(off_v) # Dmode if action == 'start': #extendcommand = Path.Command('G0', {"X": 0.0, "Y": 0.0, "Z": op.ClearanceHeight.Value}) #results.append(extendcommand) extendcommand = Path.Command('G0', {"X": leadstart.x, "Y": leadstart.y, "Z": op.ClearanceHeight.Value}) results.append(extendcommand) extendcommand = Path.Command('G0', {"Z": op.SafeHeight.Value}) results.append(extendcommand) if action == 'layer': if not obj.KeepToolDown: extendcommand = Path.Command('G0', {"Z": op.SafeHeight.Value}) results.append(extendcommand) extendcommand = Path.Command('G0', {"X": leadstart.x, "Y": leadstart.y}) results.append(extendcommand) if not obj.RapidPlunge: extendcommand = Path.Command('G1', {"X": leadstart.x, "Y": leadstart.y, "Z": p1.z, "F": vertFeed}) else: extendcommand = Path.Command('G0', {"X": leadstart.x, "Y": leadstart.y, "Z": p1.z,}) results.append(extendcommand) if obj.UseMachineCRC: if self.getDirectionOfPath(obj) == 'right': results.append(Path.Command('G42', {'D': toolnummer})) else: results.append(Path.Command('G41', {'D': toolnummer})) if obj.StyleOn == 'Arc': arcmove = Path.Command(arcdir, {"X": p0.x+vec_off.x, "Y": p0.y+vec_off.y, "I": offsetvector.x+vec_off.x, "J": offsetvector.y+vec_off.y, "F": horizFeed}) # add G2/G3 move results.append(arcmove) if obj.ExtendLeadIn != 0: extendcommand = Path.Command('G1', {"X": p0.x, "Y": p0.y, "F": horizFeed}) results.append(extendcommand) elif obj.StyleOn == 'Tangent': extendcommand = Path.Command('G1', {"X": p0.x, "Y": p0.y, "F": horizFeed}) results.append(extendcommand) else: PathLog.debug(" CURRENT_IN Perp") currLocation.update(results[-1].Parameters) currLocation['Z'] = p1.z return results
def execute(self, obj): import Part # math #DraftGeomUtils output = "" toolLoad = obj.ToolController if toolLoad is None or toolLoad.ToolNumber == 0: FreeCAD.Console.PrintError( "No Tool Controller is selected. We need a tool to build a Path." ) #return else: self.vertFeed = toolLoad.VertFeed.Value self.horizFeed = toolLoad.HorizFeed.Value self.vertRapid = toolLoad.VertRapid.Value self.horizRapid = toolLoad.HorizRapid.Value tool = toolLoad.Proxy.getTool(toolLoad) if not tool or tool.Diameter == 0: FreeCAD.Console.PrintError( "No Tool found or diameter is zero. We need a tool to build a Path." ) return else: self.radius = tool.Diameter / 2 output += "(" + obj.Label + ")" if obj.UseComp: output += "(Compensated Tool Path. Diameter: " + str( self.radius * 2) + ")" else: output += "(Uncompensated Tool Path)" if obj.Base: holes = [] faces = [] 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 numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face holes += shape.Wires[1:] else: print( "found a base object which is not a face. Can't continue." ) return profileshape = Part.makeCompound(faces) profilewire = TechDraw.findShapeOutline(profileshape, 1, Vector(0, 0, 1)) if obj.processHoles: for wire in holes: edgelist = wire.Edges edgelist = Part.__sortEdges__(edgelist) output += self._buildPathLibarea(obj, edgelist, True) if obj.processPerimeter: edgelist = profilewire.Edges edgelist = Part.__sortEdges__(edgelist) output += self._buildPathLibarea(obj, edgelist, False) else: #Try to build targets frorm the job base parentJob = PathUtils.findParentJob(obj) if parentJob is None: return baseobject = parentJob.Base if baseobject is None: return if hasattr(baseobject, "Proxy"): if isinstance(baseobject.Proxy, ArchPanel.PanelSheet): # process the sheet if obj.processPerimeter: shapes = baseobject.Proxy.getOutlines(baseobject, transform=True) for shape in shapes: for wire in shape.Wires: edgelist = wire.Edges edgelist = Part.__sortEdges__(edgelist) PathLog.debug( "Processing panel perimeter. edges found: {}" .format(len(edgelist))) try: output += self._buildPathLibarea(obj, edgelist, isHole=False) except: FreeCAD.Console.PrintError( "Something unexpected happened. Unable to generate a contour path. Check project and tool config." ) shapes = baseobject.Proxy.getHoles(baseobject, transform=True) for shape in shapes: for wire in shape.Wires: drillable = PathUtils.isDrillable( baseobject.Proxy, wire) if (drillable and obj.processCircles) or ( not drillable and obj.processHoles): edgelist = wire.Edges edgelist = Part.__sortEdges__(edgelist) try: output += self._buildPathLibarea( obj, edgelist, isHole=True) except: FreeCAD.Console.PrintError( "Something unexpected happened. Unable to generate a contour path. Check project and tool config." ) if obj.Active: path = Path.Path(output) obj.Path = path obj.ViewObject.Visibility = True else: path = Path.Path("(inactive operation)") obj.Path = path obj.ViewObject.Visibility = False
def generateTags(self, obj, count, width=None, height=None, angle=None, radius=None, spacing=None): PathLog.track(count, width, height, angle, spacing) # for e in self.baseWire.Edges: # debugMarker(e.Vertexes[0].Point, 'base', (0.0, 1.0, 1.0), 0.2) if spacing: tagDistance = spacing else: tagDistance = self.baseWire.Length / (count if count else 4) W = width if width else self.defaultTagWidth() H = height if height else self.defaultTagHeight() A = angle if angle else self.defaultTagAngle() R = radius if radius else self.defaultTagRadius() # start assigning tags on the longest segment (shortestEdge, longestEdge) = self.shortestAndLongestPathEdge() startIndex = 0 for i in range(0, len(self.baseWire.Edges)): edge = self.baseWire.Edges[i] PathLog.debug(' %d: %.2f' % (i, edge.Length)) if PathGeom.isRoughly(edge.Length, longestEdge.Length): startIndex = i break startEdge = self.baseWire.Edges[startIndex] startCount = int(startEdge.Length / tagDistance) if (longestEdge.Length - shortestEdge.Length) > shortestEdge.Length: startCount = int(startEdge.Length / tagDistance) + 1 lastTagLength = (startEdge.Length + (startCount - 1) * tagDistance) / 2 currentLength = startEdge.Length minLength = min(2. * W, longestEdge.Length) PathLog.debug( "length=%.2f shortestEdge=%.2f(%.2f) longestEdge=%.2f(%.2f) minLength=%.2f" % (self.baseWire.Length, shortestEdge.Length, shortestEdge.Length / self.baseWire.Length, longestEdge.Length, longestEdge.Length / self.baseWire.Length, minLength)) PathLog.debug( " start: index=%-2d count=%d (length=%.2f, distance=%.2f)" % (startIndex, startCount, startEdge.Length, tagDistance)) PathLog.debug(" -> lastTagLength=%.2f)" % lastTagLength) PathLog.debug(" -> currentLength=%.2f)" % currentLength) edgeDict = {startIndex: startCount} for i in range(startIndex + 1, len(self.baseWire.Edges)): edge = self.baseWire.Edges[i] (currentLength, lastTagLength) = self.processEdge(i, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict) for i in range(0, startIndex): edge = self.baseWire.Edges[i] (currentLength, lastTagLength) = self.processEdge(i, edge, currentLength, lastTagLength, tagDistance, minLength, edgeDict) tags = [] for (i, count) in PathUtil.keyValueIter(edgeDict): edge = self.baseWire.Edges[i] PathLog.debug(" %d: %d" % (i, count)) # debugMarker(edge.Vertexes[0].Point, 'base', (1.0, 0.0, 0.0), 0.2) # debugMarker(edge.Vertexes[1].Point, 'base', (0.0, 1.0, 0.0), 0.2) if 0 != count: distance = (edge.LastParameter - edge.FirstParameter) / count for j in range(0, count): tag = edge.Curve.value((j + 0.5) * distance) tags.append(Tag(j, tag.x, tag.y, W, H, A, R, True)) return tags
def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ """opExecute(obj, getsim=False) ... implementation of Path.Area ops. determines the parameters for _buildPathArea(). Do not overwrite, implement areaOpAreaParams(obj, isHole) ... op specific area param dictionary areaOpPathParams(obj, isHole) ... op specific path param dictionary areaOpShapes(obj) ... the shape for path area to process areaOpUseProjection(obj) ... return true if operation can use projection instead.""" PathLog.track() # Instantiate class variables for operation reference self.endVector = None # pylint: disable=attribute-defined-outside-init self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init # Initiate depthparams and calculate operation heights for operation self.depthparams = self._customDepthParams(obj, obj.StartDepth.Value, obj.FinalDepth.Value) # Set start point if PathOp.FeatureStartPoint & self.opFeatures( obj) and obj.UseStartPoint: start = obj.StartPoint else: start = None aOS = self.areaOpShapes(obj) # pylint: disable=assignment-from-no-return # Adjust tuples length received from other PathWB tools/operations shapes = [] for shp in aOS: if len(shp) == 2: (fc, iH) = shp # fc, iH, sub or description tup = fc, iH, "otherOp" shapes.append(tup) else: shapes.append(shp) if len(shapes) > 1: locations = [] for s in shapes: if s[2] == "OpenEdge": shp = Part.makeCompound(s[0]) else: shp = s[0] locations.append({ "x": shp.BoundBox.XMax, "y": shp.BoundBox.YMax, "shape": s }) locations = PathUtils.sort_locations(locations, ["x", "y"]) shapes = [j["shape"] for j in locations] sims = [] for shape, isHole, sub in shapes: profileEdgesIsOpen = False if sub == "OpenEdge": profileEdgesIsOpen = True if (PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint): osp = obj.StartPoint self.commandlist.append( Path.Command("G0", { "X": osp.x, "Y": osp.y, "F": self.horizRapid })) try: if profileEdgesIsOpen: (pp, sim) = self._buildProfileOpenEdges( obj, shape, isHole, start, getsim) else: (pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim) except Exception as e: # pylint: disable=broad-except FreeCAD.Console.PrintError(e) FreeCAD.Console.PrintError( "Something unexpected happened. Check project and tool config." ) else: if profileEdgesIsOpen: ppCmds = pp else: ppCmds = pp.Commands # Save gcode commands to object command list self.commandlist.extend(ppCmds) sims.append(sim) # Eif if (self.areaOpRetractTool(obj) and self.endVector is not None and len(self.commandlist) > 1): self.endVector[2] = obj.ClearanceHeight.Value self.commandlist.append( Path.Command("G0", { "Z": obj.ClearanceHeight.Value, "F": self.vertRapid })) PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n") return sims
def _buildPathArea(self, obj, baseobject, isHole, start, getsim): '''_buildPathArea(obj, baseobject, isHole, start, getsim) ... internal function.''' # pylint: disable=unused-argument PathLog.track() area = Path.Area() area.setPlane(PathUtils.makeWorkplane(baseobject)) area.add(baseobject) areaParams = self.areaOpAreaParams(obj, isHole) # pylint: disable=assignment-from-no-return heights = [i for i in self.depthparams] PathLog.debug('depths: {}'.format(heights)) area.setParams(**areaParams) obj.AreaParams = str(area.getParams()) PathLog.debug("Area with params: {}".format(area.getParams())) sections = area.makeSections(mode=0, project=self.areaOpUseProjection(obj), heights=heights) PathLog.debug("sections = %s" % sections) shapelist = [sec.getShape() for sec in sections] PathLog.debug("shapelist = %s" % shapelist) pathParams = self.areaOpPathParams(obj, isHole) # pylint: disable=assignment-from-no-return pathParams['shapes'] = shapelist pathParams['feedrate'] = self.horizFeed pathParams['feedrate_v'] = self.vertFeed pathParams['verbose'] = True pathParams['resume_height'] = obj.SafeHeight.Value pathParams['retraction'] = obj.ClearanceHeight.Value pathParams['return_end'] = True # Note that emitting preambles between moves breaks some dressups and prevents path optimization on some controllers pathParams['preamble'] = False if not self.areaOpRetractTool(obj): pathParams['threshold'] = 2.001 * self.radius if self.endVector is not None: pathParams['start'] = self.endVector elif PathOp.FeatureStartPoint & self.opFeatures( obj) and obj.UseStartPoint: pathParams['start'] = obj.StartPoint obj.PathParams = str({ key: value for key, value in pathParams.items() if key != 'shapes' }) PathLog.debug("Path with params: {}".format(obj.PathParams)) (pp, end_vector) = Path.fromShapes(**pathParams) PathLog.debug('pp: {}, end vector: {}'.format(pp, end_vector)) self.endVector = end_vector # pylint: disable=attribute-defined-outside-init simobj = None if getsim: areaParams['Thicken'] = True areaParams['ToolRadius'] = self.radius - self.radius * .005 area.setParams(**areaParams) sec = area.makeSections(mode=0, project=False, heights=heights)[-1].getShape() simobj = sec.extrude(FreeCAD.Vector(0, 0, baseobject.BoundBox.ZMax)) return pp, simobj
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 createRampMethod2(self, rampedges, p0, projectionlen, rampangle): """ This method generates ramp with following pattern: 1. Start from the original startpoint of the plunge 2. Calculate the distance on the path which is needed to implement the ramp and travel that distance while maintaining start depth 3. Start ramping while travelling the original path backwards until reaching the original plunge end point 4. Continue with the original path """ outedges = [] rampremaining = projectionlen curPoint = p0 # start from the upper point of plunge if PathGeom.pointsCoincide( PathGeom.xy(p0), PathGeom.xy(rampedges[-1].valueAt( rampedges[-1].LastParameter))): PathLog.debug( "The ramp forms a closed wire, needless to move on original Z height" ) else: for i, redge in enumerate(rampedges): if redge.Length >= rampremaining: # this edge needs to be split p1 = self.getSplitPoint(redge, rampremaining) splitEdge = PathGeom.splitEdgeAt(redge, p1) PathLog.debug( "Got split edges with lengths: {}, {}".format( splitEdge[0].Length, splitEdge[1].Length)) # ramp starts at the last point of first edge p1 = splitEdge[0].valueAt(splitEdge[0].LastParameter) p1.z = p0.z outedges.append( self.createRampEdge(splitEdge[0], curPoint, p1)) # now we have reached the beginning of the ramp. # start that by going to the beginning of this splitEdge deltaZ = splitEdge[0].Length / math.tan( math.radians(rampangle)) newPoint = FreeCAD.Base.Vector( splitEdge[0].valueAt(splitEdge[0].FirstParameter).x, splitEdge[0].valueAt(splitEdge[0].FirstParameter).y, p1.z - deltaZ) outedges.append( self.createRampEdge(splitEdge[0], p1, newPoint)) curPoint = newPoint elif i == len(rampedges) - 1: # last ramp element but still did not reach the full length? # Probably a rounding issue on floats. # Lets start the ramp anyway p1 = redge.valueAt(redge.LastParameter) p1.z = p0.z outedges.append(self.createRampEdge(redge, curPoint, p1)) # and go back that edge deltaZ = redge.Length / math.tan(math.radians(rampangle)) newPoint = FreeCAD.Base.Vector( redge.valueAt(redge.FirstParameter).x, redge.valueAt(redge.FirstParameter).y, p1.z - deltaZ) outedges.append(self.createRampEdge(redge, p1, newPoint)) curPoint = newPoint else: # we are travelling on start depth newPoint = FreeCAD.Base.Vector( redge.valueAt(redge.LastParameter).x, redge.valueAt(redge.LastParameter).y, p0.z) outedges.append( self.createRampEdge(redge, curPoint, newPoint)) curPoint = newPoint rampremaining = rampremaining - redge.Length # the last edge got handled previously rampedges.pop() # ramp backwards to the plunge position for i, redge in enumerate(reversed(rampedges)): deltaZ = redge.Length / math.tan(math.radians(rampangle)) newPoint = FreeCAD.Base.Vector( redge.valueAt(redge.FirstParameter).x, redge.valueAt(redge.FirstParameter).y, curPoint.z - deltaZ) if i == len(rampedges) - 1: # make sure that the last point of the ramps ends to the original position newPoint = redge.valueAt(redge.FirstParameter) outedges.append(self.createRampEdge(redge, curPoint, newPoint)) curPoint = newPoint return outedges
def createRampMethod3(self, rampedges, p0, projectionlen, rampangle): """ This method generates ramp with following pattern: 1. Start from the original startpoint of the plunge 2. Ramp down along the path that comes after the plunge until traveled half of the Z distance 3. Change direction and ramp backwards to the original plunge end point 4. Continue with the original path This method causes many unnecessary moves with tool down. """ outedges = [] rampremaining = projectionlen curPoint = p0 # start from the upper point of plunge done = False while not done: for i, redge in enumerate(rampedges): if redge.Length >= rampremaining: # will reach end of ramp within this edge, needs to be split p1 = self.getSplitPoint(redge, rampremaining) splitEdge = PathGeom.splitEdgeAt(redge, p1) PathLog.debug( "Got split edge (index: {}) with lengths: {}, {}". format(i, splitEdge[0].Length, splitEdge[1].Length)) # ramp ends to the last point of first edge p1 = splitEdge[0].valueAt(splitEdge[0].LastParameter) deltaZ = splitEdge[0].Length / math.tan( math.radians(rampangle)) p1.z = curPoint.z - deltaZ outedges.append( self.createRampEdge(splitEdge[0], curPoint, p1)) curPoint.z = p1.z - deltaZ # now we have reached the end of the ramp. Reverse direction of ramp # start that by going back to the beginning of this splitEdge outedges.append( self.createRampEdge(splitEdge[0], p1, curPoint)) done = True break elif i == len(rampedges) - 1: # last ramp element but still did not reach the full length? # Probably a rounding issue on floats. p1 = redge.valueAt(redge.LastParameter) deltaZ = redge.Length / math.tan(math.radians(rampangle)) p1.z = curPoint.z - deltaZ outedges.append(self.createRampEdge(redge, curPoint, p1)) # and go back that edge newPoint = FreeCAD.Base.Vector( redge.valueAt(redge.FirstParameter).x, redge.valueAt(redge.FirstParameter).y, p1.z - deltaZ) outedges.append(self.createRampEdge(redge, p1, newPoint)) curPoint = newPoint done = True else: deltaZ = redge.Length / math.tan(math.radians(rampangle)) newPoint = FreeCAD.Base.Vector( redge.valueAt(redge.LastParameter).x, redge.valueAt(redge.LastParameter).y, curPoint.z - deltaZ) outedges.append( self.createRampEdge(redge, curPoint, newPoint)) curPoint = newPoint rampremaining = rampremaining - redge.Length returnedges = self.getreversed(rampedges[:i]) # ramp backwards to the plunge position for i, redge in enumerate(returnedges): deltaZ = redge.Length / math.tan(math.radians(rampangle)) newPoint = FreeCAD.Base.Vector( redge.valueAt(redge.LastParameter).x, redge.valueAt(redge.LastParameter).y, curPoint.z - deltaZ) if i == len(rampedges) - 1: # make sure that the last point of the ramps ends to the original position newPoint = redge.valueAt(redge.LastParameter) outedges.append(self.createRampEdge(redge, curPoint, newPoint)) curPoint = newPoint return outedges
def createRampMethod1(self, rampedges, p0, projectionlen, rampangle): """ This method generates ramp with following pattern: 1. Start from the original startpoint of the plunge 2. Ramp down along the path that comes after the plunge 3. When reaching the Z level of the original plunge, return back to the beginning by going the path backwards until the original plunge end point is reached 4. Continue with the original path This method causes many unnecessary moves with tool down. """ outedges = [] rampremaining = projectionlen curPoint = p0 # start from the upper point of plunge done = False goingForward = True while not done: for i, redge in enumerate(rampedges): if redge.Length >= rampremaining: # will reach end of ramp within this edge, needs to be split p1 = self.getSplitPoint(redge, rampremaining) splitEdge = PathGeom.splitEdgeAt(redge, p1) PathLog.debug("Ramp remaining: {}".format(rampremaining)) PathLog.debug( "Got split edge (index: {}) (total len: {}) with lengths: {}, {}" .format(i, redge.Length, splitEdge[0].Length, splitEdge[1].Length)) # ramp ends to the last point of first edge p1 = splitEdge[0].valueAt(splitEdge[0].LastParameter) outedges.append( self.createRampEdge(splitEdge[0], curPoint, p1)) # now we have reached the end of the ramp. Go back to plunge position with constant Z # start that by going to the beginning of this splitEdge if goingForward: outedges.append( self.createRampEdge( splitEdge[0], p1, redge.valueAt(redge.FirstParameter))) else: # if we were reversing, we continue to the same direction as the ramp outedges.append( self.createRampEdge( splitEdge[0], p1, redge.valueAt(redge.LastParameter))) done = True break else: deltaZ = redge.Length / math.tan(math.radians(rampangle)) newPoint = FreeCAD.Base.Vector( redge.valueAt(redge.LastParameter).x, redge.valueAt(redge.LastParameter).y, curPoint.z - deltaZ) outedges.append( self.createRampEdge(redge, curPoint, newPoint)) curPoint = newPoint rampremaining = rampremaining - redge.Length if not done: # we did not reach the end of the ramp going this direction, lets reverse. rampedges = self.getreversed(rampedges) PathLog.debug("Reversing") if goingForward: goingForward = False else: goingForward = True # now we need to return to original position. if goingForward: # if the ramp was going forward, the return edges are the edges we already covered in ramping, # except the last one, which was already covered inside for loop. Direction needs to be reversed also returnedges = self.getreversed(rampedges[:i]) else: # if the ramp was already reversing, the edges needed for return are the ones # which were not covered in ramp returnedges = rampedges[(i + 1):] # add the return edges: outedges.extend(returnedges) return outedges
def generateHelix(self): edges = self.wire.Edges minZ = self.findMinZ(edges) outedges = [] i = 0 while i < len(edges): edge = edges[i] 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 if bb.XLength < 1e-6 and bb.YLength < 1e-6 and bb.ZLength > 0 and p0.z > p1.z: # plungelen = abs(p0.z-p1.z) PathLog.debug( "Found plunge move at X:{} Y:{} From Z:{} to Z{}, Searching for closed loop" .format(p0.x, p0.y, p0.z, p1.z)) # check if above ignoreAbove parameter - do not generate helix if it is newEdge, cont = self.checkIgnoreAbove(edge) if newEdge is not None: outedges.append(newEdge) p0.z = self.ignoreAbove if cont: i = i + 1 continue # next need to determine how many edges in the path after plunge are needed to cover the length: loopFound = False rampedges = [] j = i + 1 while not loopFound: candidate = edges[j] cp0 = candidate.Vertexes[0].Point cp1 = candidate.Vertexes[1].Point if PathGeom.pointsCoincide(p1, cp1): # found closed loop loopFound = True rampedges.append(candidate) break 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) j = j + 1 if j >= len(edges): break if len(rampedges) == 0 or not loopFound: PathLog.debug("No suitable helix found") outedges.append(edge) else: outedges.extend(self.createHelix(rampedges, p0, p1)) if not PathGeom.isRoughly(p1.z, minZ): # the edges covered by the helix not handled again, # unless reached the bottom height i = j else: outedges.append(edge) else: outedges.append(edge) i = i + 1 return outedges
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) ... 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 findHoles(self, obj, shape): import DraftGeomUtils as dgu PathLog.track('obj: {} shape: {}'.format(obj, shape)) holelist = [] tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter PathLog.debug('search for holes larger than tooldiameter: {}: '.format(tooldiameter)) if dgu.isPlanar(shape): PathLog.debug("shape is planar") for i in range(len(shape.Edges)): candidateEdgeName = "Edge" + str(i +1) e = shape.getElement(candidateEdgeName) if PathUtils.isDrillable(shape, e, tooldiameter): PathLog.debug('edge candidate: {} is drillable '.format(e)) x = e.Curve.Center.x y = e.Curve.Center.y diameter = e.BoundBox.XLength holelist.append((candidateEdgeName, e, x, y, diameter)) else: PathLog.debug("shape is not planar") for i in range(len(shape.Faces)): candidateFaceName = "Face" + str(i + 1) f = shape.getElement(candidateFaceName) if PathUtils.isDrillable(shape, f, tooldiameter): PathLog.debug('face candidate: {} is drillable '.format(f)) x = f.Surface.Center.x y = f.Surface.Center.y diameter = f.BoundBox.XLength holelist.append((candidateFaceName, f, x, y, diameter)) PathLog.debug("holes found: {}".format(holelist)) return holelist
def opSetDefaultValues(self, obj, job): '''opSetDefaultValues(obj) ... base implementation, do not overwrite. The base implementation sets the depths and heights based on the areaOpShapeForDepths() return value. Do not overwrite, overwrite areaOpSetDefaultValues(obj, job) instead.''' PathLog.debug("opSetDefaultValues(%s, %s)" % (obj.Label, job.Label)) # Initial setting for EnableRotation is taken from Job settings/SetupSheet # User may override on per-operation basis as needed. if hasattr(job.SetupSheet, 'SetupEnableRotation'): obj.EnableRotation = job.SetupSheet.SetupEnableRotation else: obj.EnableRotation = 'Off' PathLog.debug("opSetDefaultValues(): Enable Rotation: {}".format( obj.EnableRotation)) if PathOp.FeatureDepths & self.opFeatures(obj): try: shape = self.areaOpShapeForDepths(obj, job) except Exception as ee: # pylint: disable=broad-except PathLog.error(ee) shape = None # Set initial start and final depths if shape is None: PathLog.debug("shape is None") startDepth = 1.0 finalDepth = 0.0 else: bb = job.Stock.Shape.BoundBox startDepth = bb.ZMax finalDepth = bb.ZMin # Adjust start and final depths if rotation is enabled if obj.EnableRotation != 'Off': self.initWithRotation = True self.stockBB = PathUtils.findParentJob( obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init # Calculate rotational distances/radii opHeights = self.opDetermineRotationRadii( obj ) # return is list with tuples [(xRotRad, yRotRad, zRotRad), (clrOfst, safOfset)] (xRotRad, yRotRad, zRotRad) = opHeights[0] # pylint: disable=unused-variable PathLog.debug("opHeights[0]: " + str(opHeights[0])) PathLog.debug("opHeights[1]: " + str(opHeights[1])) if obj.EnableRotation == 'A(x)': startDepth = xRotRad if obj.EnableRotation == 'B(y)': startDepth = yRotRad else: startDepth = max(xRotRad, yRotRad) finalDepth = -1 * startDepth # Manage operation start and final depths if self.docRestored is True: # This op is NOT the first in the Operations list PathLog.debug("Doc restored") obj.FinalDepth.Value = obj.OpFinalDepth.Value obj.StartDepth.Value = obj.OpStartDepth.Value else: PathLog.debug("New operation") obj.StartDepth.Value = startDepth obj.FinalDepth.Value = finalDepth obj.OpStartDepth.Value = startDepth obj.OpFinalDepth.Value = finalDepth if obj.EnableRotation != 'Off': if self.initOpFinalDepth is None: self.initOpFinalDepth = finalDepth PathLog.debug("Saved self.initOpFinalDepth") if self.initOpStartDepth is None: self.initOpStartDepth = startDepth PathLog.debug("Saved self.initOpStartDepth") self.defValsSet = True PathLog.debug( "Default OpDepths are Start: {}, and Final: {}".format( obj.OpStartDepth.Value, obj.OpFinalDepth.Value)) PathLog.debug("Default Depths are Start: {}, and Final: {}".format( startDepth, finalDepth)) self.areaOpSetDefaultValues(obj, job)
def faceRotationAnalysis(self, obj, norm, surf): '''faceRotationAnalysis(obj, norm, surf) Determine X and Y independent rotation necessary to make normalAt = Z=1 (0,0,1) ''' PathLog.track() praInfo = "faceRotationAnalysis(): " rtn = True orientation = 'X' angle = 500.0 precision = 6 for i in range(0, 13): if PathGeom.Tolerance * (i * 10) == 1.0: precision = i break def roundRoughValues(precision, val): # Convert VALxe-15 numbers to zero if PathGeom.isRoughly(0.0, val) is True: return 0.0 # Convert VAL.99999999 to next integer elif math.fabs(val % 1) > 1.0 - PathGeom.Tolerance: return round(val) else: return round(val, precision) nX = roundRoughValues(precision, norm.x) nY = roundRoughValues(precision, norm.y) nZ = roundRoughValues(precision, norm.z) praInfo += "\n -normalAt(0,0): " + str(nX) + ", " + str( nY) + ", " + str(nZ) saX = roundRoughValues(precision, surf.x) saY = roundRoughValues(precision, surf.y) saZ = roundRoughValues(precision, surf.z) praInfo += "\n -Surface.Axis: " + str(saX) + ", " + str( saY) + ", " + str(saZ) # Determine rotation needed and current orientation if saX == 0.0: if saY == 0.0: orientation = "Z" if saZ == 1.0: angle = 0.0 elif saZ == -1.0: angle = -180.0 else: praInfo += "_else_X" + str(saZ) elif saY == 1.0: orientation = "Y" angle = 90.0 elif saY == -1.0: orientation = "Y" angle = -90.0 else: if saZ != 0.0: angle = math.degrees(math.atan(saY / saZ)) orientation = "Y" elif saY == 0.0: if saZ == 0.0: orientation = "X" if saX == 1.0: angle = -90.0 elif saX == -1.0: angle = 90.0 else: praInfo += "_else_X" + str(saX) else: orientation = "X" ratio = saX / saZ angle = math.degrees(math.atan(ratio)) if ratio < 0.0: praInfo += " NEG-ratio" # angle -= 90 else: praInfo += " POS-ratio" angle = -1 * angle if saX < 0.0: angle = angle + 180.0 elif saZ == 0.0: if saY != 0.0: angle = math.degrees(math.atan(saX / saY)) orientation = "Y" if saX + nX == 0.0: angle = -1 * angle if saY + nY == 0.0: angle = -1 * angle if saZ + nZ == 0.0: angle = -1 * angle if saY == -1.0 or saY == 1.0: if nX != 0.0: angle = -1 * angle # Enforce enabled rotation in settings praInfo += "\n -Initial orientation: {}".format(orientation) if orientation == 'Y': axis = 'X' if obj.EnableRotation == 'B(y)': # Required axis disabled if angle == 180.0 or angle == -180.0: axis = 'Y' else: rtn = False elif orientation == 'X': axis = 'Y' if obj.EnableRotation == 'A(x)': # Required axis disabled if angle == 180.0 or angle == -180.0: axis = 'X' else: rtn = False elif orientation == 'Z': axis = 'X' if math.fabs(angle) == 0.0: angle = 0.0 rtn = False if angle == 500.0: angle = 0.0 rtn = False if rtn is False: if orientation == 'Z' and angle == 0.0 and obj.ReverseDirection is True: if obj.EnableRotation == 'B(y)': axis = 'Y' rtn = True if rtn is True: self.rotateFlag = True # pylint: disable=attribute-defined-outside-init # rtn = True if obj.ReverseDirection is True: if angle < 180.0: angle = angle + 180.0 else: angle = angle - 180.0 angle = round(angle, precision) praInfo += "\n -Rotation analysis: angle: " + str( angle) + ", axis: " + str(axis) if rtn is True: praInfo += "\n - ... rotation triggered" else: praInfo += "\n - ... NO rotation triggered" PathLog.debug("\n" + str(praInfo)) return (rtn, angle, axis, praInfo)
def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ '''opExecute(obj, getsim=False) ... implementation of Path.Area ops. determines the parameters for _buildPathArea(). Do not overwrite, implement areaOpAreaParams(obj, isHole) ... op specific area param dictionary areaOpPathParams(obj, isHole) ... op specific path param dictionary areaOpShapes(obj) ... the shape for path area to process areaOpUseProjection(obj) ... return true if operation can use projection instead.''' PathLog.track() # Instantiate class variables for operation reference self.endVector = None # pylint: disable=attribute-defined-outside-init self.rotateFlag = False # pylint: disable=attribute-defined-outside-init self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init self.cloneNames = [] # pylint: disable=attribute-defined-outside-init self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init self.useTempJobClones( 'Delete') # Clear temporary group and recreate for temp job clones # Import OpFinalDepth from pre-existing operation for recompute() scenarios if self.defValsSet is True: PathLog.debug("self.defValsSet is True.") if self.initOpStartDepth is not None: if self.initOpStartDepth != obj.OpStartDepth.Value: obj.OpStartDepth.Value = self.initOpStartDepth obj.StartDepth.Value = self.initOpStartDepth if self.initOpFinalDepth is not None: if self.initOpFinalDepth != obj.OpFinalDepth.Value: obj.OpFinalDepth.Value = self.initOpFinalDepth obj.FinalDepth.Value = self.initOpFinalDepth self.defValsSet = False if obj.EnableRotation != 'Off': # 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 (self.clrOfset, self.safOfst) = opHeights[1] # pylint: disable=attribute-defined-outside-init # Set clearnance 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 + self.clrOfset obj.SafeHeight.Value = strDep + self.safOfst if self.initWithRotation is False: if obj.FinalDepth.Value == obj.OpFinalDepth.Value: obj.FinalDepth.Value = finDep if obj.StartDepth.Value == obj.OpStartDepth.Value: obj.StartDepth.Value = strDep # Create visual axes when debugging. if PathLog.getLevel(PathLog.thisModule()) == 4: self.visualAxis() else: strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value # 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 # Initiate depthparams and calculate operation heights for rotational operation finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 self.depthparams = PathUtils.depth_params( # pylint: disable=attribute-defined-outside-init 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) # Set start point if PathOp.FeatureStartPoint & self.opFeatures( obj) and obj.UseStartPoint: start = obj.StartPoint else: start = None aOS = self.areaOpShapes(obj) # pylint: disable=assignment-from-no-return # Adjust tuples length received from other PathWB tools/operations beside PathPocketShape shapes = [] for shp in aOS: if len(shp) == 2: (fc, iH) = shp # fc, iH, sub, angle, axis tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) else: shapes.append(shp) if len(shapes) > 1: jobs = [{ 'x': s[0].BoundBox.XMax, 'y': s[0].BoundBox.YMax, 'shape': s } for s in shapes] jobs = PathUtils.sort_jobs(jobs, ['x', 'y']) shapes = [j['shape'] for j in jobs] # PathLog.debug("Pre_path depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value)) sims = [] numShapes = len(shapes) # if numShapes == 1: # nextAxis = shapes[0][4] # elif numShapes > 1: # nextAxis = shapes[1][4] # else: # nextAxis = 'L' for ns in range(0, numShapes): (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable if ns < numShapes - 1: nextAxis = shapes[ns + 1][4] else: nextAxis = 'L' finish_step = obj.FinishDepth.Value if hasattr( obj, "FinishDepth") else 0.0 self.depthparams = PathUtils.depth_params( # pylint: disable=attribute-defined-outside-init 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) try: (pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim) except Exception as e: # pylint: disable=broad-except FreeCAD.Console.PrintError(e) FreeCAD.Console.PrintError( "Something unexpected happened. Check project and tool config." ) else: ppCmds = pp.Commands if obj.EnableRotation != 'Off' and self.rotateFlag is True: # Rotate model to index for cut if axis == 'X': axisOfRot = 'A' elif axis == 'Y': axisOfRot = 'B' # Reverse angle temporarily to match model. Error in FreeCAD render of B axis rotations if obj.B_AxisErrorOverride is True: angle = -1 * angle elif axis == 'Z': axisOfRot = 'C' else: axisOfRot = 'A' # Rotate Model to correct angle ppCmds.insert( 0, Path.Command('G1', { axisOfRot: angle, 'F': self.axialFeed })) ppCmds.insert(0, Path.Command('N100', {})) # Raise cutter to safe depth and return index to starting position ppCmds.append(Path.Command('N200', {})) ppCmds.append( Path.Command('G0', { 'Z': obj.SafeHeight.Value, 'F': self.vertRapid })) if axis != nextAxis: ppCmds.append( Path.Command('G0', { axisOfRot: 0.0, 'F': self.axialRapid })) # Eif # Save gcode commands to object command list self.commandlist.extend(ppCmds) sims.append(sim) # Eif if self.areaOpRetractTool(obj): self.endVector = None # pylint: disable=attribute-defined-outside-init # Raise cutter to safe height and rotate back to original orientation if self.rotateFlag is True: self.commandlist.append( Path.Command('G0', { 'Z': obj.SafeHeight.Value, 'F': self.vertRapid })) self.commandlist.append( Path.Command('G0', { 'A': 0.0, 'F': self.axialRapid })) self.commandlist.append( Path.Command('G0', { 'B': 0.0, 'F': self.axialRapid })) 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) + "\n\n") return sims
def findHoles(self, obj, baseobject): '''findHoles(obj, baseobject) ... inspect baseobject and identify all features that resemble a straight cricular hole.''' shape = baseobject.Shape PathLog.track('obj: {} shape: {}'.format(obj, shape)) holelist = [] features = [] # tooldiameter = obj.ToolController.Proxy.getTool(obj.ToolController).Diameter tooldiameter = None PathLog.debug('search for holes larger than tooldiameter: {}: '.format( tooldiameter)) if DraftGeomUtils.isPlanar(shape): PathLog.debug("shape is planar") for i in range(len(shape.Edges)): candidateEdgeName = "Edge" + str(i + 1) e = shape.getElement(candidateEdgeName) if PathUtils.isDrillable(shape, e, tooldiameter): PathLog.debug( 'edge candidate: {} (hash {})is drillable '.format( e, e.hashCode())) x = e.Curve.Center.x y = e.Curve.Center.y diameter = e.BoundBox.XLength holelist.append({ 'featureName': candidateEdgeName, 'feature': e, 'x': x, 'y': y, 'd': diameter, 'enabled': True }) features.append((baseobject, candidateEdgeName)) PathLog.debug("Found hole feature %s.%s" % (baseobject.Label, candidateEdgeName)) else: PathLog.debug("shape is not planar") for i in range(len(shape.Faces)): candidateFaceName = "Face" + str(i + 1) f = shape.getElement(candidateFaceName) if PathUtils.isDrillable(shape, f, tooldiameter): PathLog.debug('face candidate: {} is drillable '.format(f)) x = f.Surface.Center.x y = f.Surface.Center.y diameter = f.BoundBox.XLength holelist.append({ 'featureName': candidateFaceName, 'feature': f, 'x': x, 'y': y, 'd': diameter, 'enabled': True }) features.append((baseobject, candidateFaceName)) PathLog.debug("Found hole feature %s.%s" % (baseobject.Label, candidateFaceName)) PathLog.debug("holes found: {}".format(holelist)) return features
def createPath(self, obj, pathData, tags): PathLog.track() commands = [] lastEdge = 0 lastTag = 0 # sameTag = None t = 0 # inters = None edge = None segm = 50 if hasattr(obj, 'SegmentationFactor'): segm = obj.SegmentationFactor if segm <= 0: segm = 50 obj.SegmentationFactor = 50 self.mappers = [] mapper = None while edge or lastEdge < len(pathData.edges): PathLog.debug("------- lastEdge = %d/%d.%d/%d" % (lastEdge, lastTag, t, len(tags))) if not edge: edge = pathData.edges[lastEdge] debugEdge( edge, "======= new edge: %d/%d" % (lastEdge, len(pathData.edges))) lastEdge += 1 # sameTag = None if mapper: mapper.add(edge) if mapper.mappingComplete(): commands.extend(mapper.commands) edge = mapper.tail mapper = None else: edge = None if edge: tIndex = (t + lastTag) % len(tags) t += 1 i = tags[tIndex].intersects(edge, edge.FirstParameter) if i and self.isValidTagStartIntersection(edge, i): mapper = MapWireToTag(edge, tags[tIndex], i, segm, pathData.maxZ) self.mappers.append(mapper) edge = mapper.tail if not mapper and t >= len(tags): # gone through all tags, consume edge and move on if edge: debugEdge(edge, '++++++++') if pathData.rapid.isRapid(edge): v = edge.Vertexes[1] if not commands and PathGeom.isRoughly( 0, v.X) and PathGeom.isRoughly( 0, v.Y) and not PathGeom.isRoughly(0, v.Z): # The very first move is just to move to ClearanceHeight commands.append(Path.Command('G0', {'Z': v.Z})) else: commands.append( Path.Command('G0', { 'X': v.X, 'Y': v.Y, 'Z': v.Z })) else: commands.extend(PathGeom.cmdsForEdge(edge, segm=segm)) edge = None t = 0 lastCmd = Path.Command('G0', {'X': 0.0, 'Y': 0.0, 'Z': 0.0}) outCommands = [] tc = PathDressup.toolController(obj.Base) horizFeed = tc.HorizFeed.Value vertFeed = tc.VertFeed.Value horizRapid = tc.HorizRapid.Value vertRapid = tc.VertRapid.Value for cmd in commands: params = cmd.Parameters zVal = params.get('Z', None) zVal2 = lastCmd.Parameters.get('Z', None) zVal = zVal and round(zVal, 8) zVal2 = zVal2 and round(zVal2, 8) if cmd.Name in ['G1', 'G2', 'G3', 'G01', 'G02', 'G03']: if False and zVal is not None and zVal2 != zVal: params['F'] = vertFeed else: params['F'] = horizFeed lastCmd = cmd elif cmd.Name in ['G0', 'G00']: if zVal is not None and zVal2 != zVal: params['F'] = vertRapid else: params['F'] = horizRapid lastCmd = cmd outCommands.append(Path.Command(cmd.Name, params)) return Path.Path(outCommands)
def execute(self, obj): PathLog.track() if not obj.Active: path = Path.Path("(inactive operation)") obj.Path = path obj.ViewObject.Visibility = False return commandlist = [] toolLoad = obj.ToolController self.depthparams = depth_params( clearance_height=obj.ClearanceHeight.Value, safe_height=obj.SafeHeight.Value, start_depth=obj.StartDepth.Value, step_down=obj.StepDown, z_finish_step=obj.FinishDepth.Value, final_depth=obj.FinalDepth.Value, user_depths=None) if toolLoad is None or toolLoad.ToolNumber == 0: FreeCAD.Console.PrintError( "No Tool Controller is selected. We need a tool to build a Path." ) return else: self.vertFeed = toolLoad.VertFeed.Value self.horizFeed = toolLoad.HorizFeed.Value self.vertRapid = toolLoad.VertRapid.Value self.horizRapid = toolLoad.HorizRapid.Value tool = toolLoad.Proxy.getTool(toolLoad) if tool.Diameter == 0: FreeCAD.Console.PrintError( "No Tool found or diameter is zero. We need a tool to build a Path." ) return else: self.radius = tool.Diameter / 2 commandlist.append(Path.Command("(" + obj.Label + ")")) # Facing is done either against base objects if obj.Base: PathLog.debug("obj.Base: {}".format(obj.Base)) faces = [] for b in obj.Base: for sub in b[1]: shape = getattr(b[0].Shape, sub) if isinstance(shape, Part.Face): faces.append(shape) else: PathLog.debug('The base subobject is not a face') return planeshape = Part.makeCompound(faces) PathLog.info("Working on a collection of faces {}".format(faces)) # If no base object, do planing of top surface of entire model else: parentJob = PathUtils.findParentJob(obj) if parentJob is None: PathLog.debug("No base object. No parent job found") return baseobject = parentJob.Base if baseobject is None: PathLog.debug("Parent job exists but no Base Object") return planeshape = baseobject.Shape PathLog.info("Working on a shape {}".format(baseobject.Name)) # Let's start by rapid to clearance...just for safety commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) # if user wants the boundbox, calculate that PathLog.info("Boundary Shape: {}".format(obj.BoundaryShape)) bb = planeshape.BoundBox 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) else: env = PathUtils.getEnvelope(partshape=planeshape, depthparams=self.depthparams) try: commandlist.extend(self._buildPathArea(obj, env).Commands) except Exception as e: FreeCAD.Console.PrintError(e) FreeCAD.Console.PrintError( translate( "Path_MillFace", "The selected settings did not produce a valid path.\n")) # Let's finish by rapid to clearance...just for safety commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) path = Path.Path(commandlist) obj.Path = path obj.ViewObject.Visibility = True
def setFields(self): self.form.startDepth.setText( FreeCAD.Units.Quantity(self.obj.StartDepth.Value, FreeCAD.Units.Length).UserString) self.form.finalDepth.setText( FreeCAD.Units.Quantity(self.obj.FinalDepth.Value, FreeCAD.Units.Length).UserString) self.form.safeHeight.setText( FreeCAD.Units.Quantity(self.obj.SafeHeight.Value, FreeCAD.Units.Length).UserString) self.form.clearanceHeight.setText( FreeCAD.Units.Quantity(self.obj.ClearanceHeight.Value, FreeCAD.Units.Length).UserString) self.form.stepDown.setText( FreeCAD.Units.Quantity(self.obj.StepDown.Value, FreeCAD.Units.Length).UserString) self.form.extraOffset.setText( FreeCAD.Units.Quantity(self.obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) self.form.rollRadius.setText( FreeCAD.Units.Quantity(self.obj.RollRadius.Value, FreeCAD.Units.Length).UserString) self.form.useCompensation.setChecked(self.obj.UseComp) self.form.useStartPoint.setChecked(self.obj.UseStartPoint) self.form.useEndPoint.setChecked(self.obj.UseEndPoint) self.form.processHoles.setChecked(self.obj.processHoles) self.form.processPerimeter.setChecked(self.obj.processPerimeter) self.form.processCircles.setChecked(self.obj.processCircles) index = self.form.cutSide.findText(self.obj.Side, QtCore.Qt.MatchFixedString) if index >= 0: self.form.cutSide.blockSignals(True) self.form.cutSide.setCurrentIndex(index) self.form.cutSide.blockSignals(False) index = self.form.direction.findText(self.obj.Direction, QtCore.Qt.MatchFixedString) if index >= 0: self.form.direction.blockSignals(True) self.form.direction.setCurrentIndex(index) self.form.direction.blockSignals(False) controllers = PathUtils.getToolControllers(self.obj) labels = [c.Label for c in controllers] self.form.uiToolController.blockSignals(True) self.form.uiToolController.addItems(labels) self.form.uiToolController.blockSignals(False) if self.obj.ToolController is not None: index = self.form.uiToolController.findText( self.obj.ToolController.Label, QtCore.Qt.MatchFixedString) PathLog.debug("searching for TC label {}. Found Index: {}".format( self.obj.ToolController.Label, index)) if index >= 0: self.form.uiToolController.blockSignals(True) self.form.uiToolController.setCurrentIndex(index) self.form.uiToolController.blockSignals(False) else: self.obj.ToolController = PathUtils.findToolController(self.obj) self.form.baseList.blockSignals(True) for i in self.obj.Base: for sub in i[1]: self.form.baseList.addItem(i[0].Name + "." + sub) self.form.baseList.blockSignals(False) self.form.update()
def Activated(self): PathLog.track() FreeCAD.ActiveDocument.openTransaction( translate("Path_Post", "Post Process the Selected path(s)")) FreeCADGui.addModule("PathScripts.PathPost") # Attempt to figure out what the user wants to post-process # If a job is selected, post that. # If there's only one job in a document, post it. # If a user has selected a subobject of a job, post the job. # If multiple jobs and can't guess, ask them. selected = FreeCADGui.Selection.getSelectionEx() if len(selected) > 1: FreeCAD.Console.PrintError("Please select a single job or other path object\n") return elif len(selected) == 1: sel = selected[0].Object if sel.Name[:3] == "Job": job = sel elif hasattr(sel, "Path"): try: job = PathUtils.findParentJob(sel) except Exception: # pylint: disable=broad-except job = None else: job = None if job is None: targetlist = [] for o in FreeCAD.ActiveDocument.Objects: if hasattr(o, "Proxy"): if isinstance(o.Proxy, PathJob.ObjectJob): targetlist.append(o.Label) PathLog.debug("Possible post objects: {}".format(targetlist)) if len(targetlist) > 1: form = FreeCADGui.PySideUic.loadUi(":/panels/DlgJobChooser.ui") form.cboProject.addItems(targetlist) r = form.exec_() if r is False: return else: jobname = form.cboProject.currentText() else: jobname = targetlist[0] job = FreeCAD.ActiveDocument.getObject(jobname) PathLog.debug("about to postprocess job: {}".format(job.Name)) # Build up an ordered list of operations and tool changes. # Then post-the ordered list if hasattr(job, "Fixtures"): wcslist = job.Fixtures else: wcslist = ['G54'] if hasattr(job, "OrderOutputBy"): orderby = job.OrderOutputBy else: orderby = "Operation" if hasattr(job, "SplitOutput"): split = job.SplitOutput else: split = False postlist = [] if orderby == 'Fixture': PathLog.debug("Ordering by Fixture") # Order by fixture means all operations and tool changes will be completed in one # fixture before moving to the next. currTool = None for index, f in enumerate(wcslist): # create an object to serve as the fixture path fobj = _TempObject() c1 = Path.Command(f) fobj.Path = Path.Path([c1]) if index != 0: c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) fobj.Path.addCommands(c2) fobj.InList.append(job) sublist = [fobj] # Now generate the gcode for obj in job.Operations.Group: tc = PathUtil.toolControllerForOp(obj) if tc is not None and PathUtil.opProperty(obj, 'Active'): if tc.ToolNumber != currTool: sublist.append(tc) PathLog.debug("Appending TC: {}".format(tc.Name)) currTool = tc.ToolNumber sublist.append(obj) postlist.append(sublist) elif orderby == 'Tool': PathLog.debug("Ordering by Tool") # Order by tool means tool changes are minimized. # all operations with the current tool are processed in the current # fixture before moving to the next fixture. currTool = None fixturelist = [] for f in wcslist: # create an object to serve as the fixture path fobj = _TempObject() c1 = Path.Command(f) c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) fobj.Path = Path.Path([c1, c2]) fobj.InList.append(job) fixturelist.append(fobj) # Now generate the gcode curlist = [] # list of ops for tool, will repeat for each fixture sublist = [] # list of ops for output splitting for idx, obj in enumerate(job.Operations.Group): # check if the operation is active active = PathUtil.opProperty(obj, 'Active') tc = PathUtil.toolControllerForOp(obj) if tc is None or tc.ToolNumber == currTool and active: curlist.append(obj) elif tc.ToolNumber != currTool and currTool is None and active: # first TC sublist.append(tc) curlist.append(obj) currTool = tc.ToolNumber elif tc.ToolNumber != currTool and currTool is not None and active: # TC for fixture in fixturelist: sublist.append(fixture) sublist.extend(curlist) postlist.append(sublist) sublist = [tc] curlist = [obj] currTool = tc.ToolNumber if idx == len(job.Operations.Group) - 1: # Last operation. for fixture in fixturelist: sublist.append(fixture) sublist.extend(curlist) postlist.append(sublist) elif orderby == 'Operation': PathLog.debug("Ordering by Operation") # Order by operation means ops are done in each fixture in # sequence. currTool = None firstFixture = True # Now generate the gcode for obj in job.Operations.Group: if PathUtil.opProperty(obj, 'Active'): sublist = [] PathLog.debug("obj: {}".format(obj.Name)) for f in wcslist: fobj = _TempObject() c1 = Path.Command(f) fobj.Path = Path.Path([c1]) if not firstFixture: c2 = Path.Command("G0 Z" + str(job.Stock.Shape.BoundBox.ZMax + job.SetupSheet.ClearanceHeightOffset.Value)) fobj.Path.addCommands(c2) fobj.InList.append(job) sublist.append(fobj) firstFixture = False tc = PathUtil.toolControllerForOp(obj) if tc is not None: if tc.ToolNumber != currTool: sublist.append(tc) currTool = tc.ToolNumber sublist.append(obj) postlist.append(sublist) fail = True rc = '' # pylint: disable=unused-variable if split: for slist in postlist: (fail, rc, filename) = self.exportObjectsWith(slist, job) else: finalpostlist = [item for slist in postlist for item in slist] (fail, rc, filename) = self.exportObjectsWith(finalpostlist, job) self.subpart = 1 if fail: FreeCAD.ActiveDocument.abortTransaction() else: if hasattr(job, "LastPostProcessDate"): job.LastPostProcessDate = str(datetime.now()) if hasattr(job, "LastPostProcessOutput"): job.LastPostProcessOutput = filename FreeCAD.ActiveDocument.commitTransaction() FreeCAD.ActiveDocument.recompute()
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)) if list == type(obj): for shape in obj: if isDrillable(shape, candidate, tooldiameter, includePartials): return (True, shape) return (False, None) 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 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.info(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 cmdsForEdge(edge, flip = False, useHelixForBSpline = True, segm = 50): """(edge, flip=False, useHelixForBSpline=True, segm=50) -> List(Path.Command) Returns a list of Path.Command representing the given edge. If flip is True the edge is considered to be backwards. If useHelixForBSpline is True an Edge based on a BSplineCurve is considered to represent a helix and results in G2 or G3 command. Otherwise edge has no direct Path.Command mapping and will be approximated by straight segments. segm is a factor for the segmentation of arbitrary curves not mapped to G1/2/3 commands. The higher the value the more segments will be used.""" pt = edge.valueAt(edge.LastParameter) if not flip else edge.valueAt(edge.FirstParameter) params = {'X': pt.x, 'Y': pt.y, 'Z': pt.z} if type(edge.Curve) == Part.Line or type(edge.Curve) == Part.LineSegment: commands = [Path.Command('G1', params)] else: p1 = edge.valueAt(edge.FirstParameter) if not flip else edge.valueAt(edge.LastParameter) p2 = edge.valueAt((edge.FirstParameter + edge.LastParameter)/2) p3 = pt if (type(edge.Curve) == Part.Circle and isRoughly(edge.Curve.Axis.x, 0) and isRoughly(edge.Curve.Axis.y, 0)) or (useHelixForBSpline and type(edge.Curve) == Part.BSplineCurve): # This is an arc or a helix and it should be represented by a simple G2/G3 command if edge.Curve.Axis.z < 0: cmd = 'G2' if not flip else 'G3' else: cmd = 'G3' if not flip else 'G2' if pointsCoincide(p1, p3): # A full circle offset = edge.Curve.Center - pt else: pd = Part.Circle(xy(p1), xy(p2), xy(p3)).Center PathLog.debug("**** %s.%d: (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f) -> center=(%.2f, %.2f)" % (cmd, flip, p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, p3.x, p3.y, p3.z, pd.x, pd.y)) # Have to calculate the center in the XY plane, using pd leads to an error if this is a helix pa = xy(p1) pb = xy(p2) pc = xy(p3) offset = Part.Circle(pa, pb, pc).Center - pa PathLog.debug("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pa.x, pa.y, pa.z, pc.x, pc.y, pc.z)) PathLog.debug("**** (%.2f, %.2f, %.2f) - (%.2f, %.2f, %.2f)" % (pb.x, pb.y, pb.z, pd.x, pd.y, pd.z)) PathLog.debug("**** (%.2f, %.2f, %.2f)" % (offset.x, offset.y, offset.z)) params.update({'I': offset.x, 'J': offset.y, 'K': (p3.z - p1.z)/2}) commands = [ Path.Command(cmd, params) ] else: # We're dealing with a helix or a more complex shape and it has to get approximated # by a number of straight segments eStraight = Part.Edge(Part.LineSegment(p1, p3)) esP2 = eStraight.valueAt((eStraight.FirstParameter + eStraight.LastParameter)/2) deviation = (p2 - esP2).Length if isRoughly(deviation, 0): return [ Path.Command('G1', {'X': p3.x, 'Y': p3.y, 'Z': p3.z}) ] # at this point pixellation is all we can do commands = [] segments = int(math.ceil((deviation / eStraight.Length) * segm)) #print("**** pixellation with %d segments" % segments) dParameter = (edge.LastParameter - edge.FirstParameter) / segments for i in range(0, segments): if flip: p = edge.valueAt(edge.LastParameter - (i + 1) * dParameter) else: p = edge.valueAt(edge.FirstParameter + (i + 1) * dParameter) cmd = Path.Command('G1', {'X': p.x, 'Y': p.y, 'Z': p.z}) #print("***** %s" % cmd) commands.append(cmd) #print commands return commands
def opExecute(self, obj, getsim=False): # pylint: disable=arguments-differ '''opExecute(obj, getsim=False) ... implementation of Path.Area ops. determines the parameters for _buildPathArea(). Do not overwrite, implement areaOpAreaParams(obj, isHole) ... op specific area param dictionary areaOpPathParams(obj, isHole) ... op specific path param dictionary areaOpShapes(obj) ... the shape for path area to process areaOpUseProjection(obj) ... return true if operation can use projection instead.''' PathLog.track() # Instantiate class variables for operation reference self.endVector = None # pylint: disable=attribute-defined-outside-init self.rotateFlag = False # pylint: disable=attribute-defined-outside-init self.leadIn = 2.0 # pylint: disable=attribute-defined-outside-init self.cloneNames = [] # pylint: disable=attribute-defined-outside-init self.guiMsgs = [] # pylint: disable=attribute-defined-outside-init self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init self.useTempJobClones( 'Delete') # Clear temporary group and recreate for temp job clones self.rotStartDepth = None # pylint: disable=attribute-defined-outside-init if obj.EnableRotation != 'Off': # 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 (self.clrOfset, self.safOfst) = opHeights[1] # pylint: disable=attribute-defined-outside-init # 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 self.rotStartDepth = strDep obj.ClearanceHeight.Value = strDep + self.clrOfset obj.SafeHeight.Value = strDep + self.safOfst # Create visual axes when debugging. if PathLog.getLevel(PathLog.thisModule()) == 4: self.visualAxis() else: strDep = obj.StartDepth.Value finDep = obj.FinalDepth.Value # 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 # Initiate depthparams and calculate operation heights for rotational operation self.depthparams = self._customDepthParams(obj, obj.StartDepth.Value, obj.FinalDepth.Value) # Set start point if PathOp.FeatureStartPoint & self.opFeatures( obj) and obj.UseStartPoint: start = obj.StartPoint else: start = None aOS = self.areaOpShapes(obj) # pylint: disable=assignment-from-no-return # Adjust tuples length received from other PathWB tools/operations beside PathPocketShape shapes = [] for shp in aOS: if len(shp) == 2: (fc, iH) = shp # fc, iH, sub, angle, axis, strtDep, finDep tup = fc, iH, 'otherOp', 0.0, 'S', obj.StartDepth.Value, obj.FinalDepth.Value shapes.append(tup) else: shapes.append(shp) if len(shapes) > 1: jobs = list() for s in shapes: if s[2] == 'OpenEdge': shp = Part.makeCompound(s[0]) else: shp = s[0] jobs.append({ 'x': shp.BoundBox.XMax, 'y': shp.BoundBox.YMax, 'shape': s }) jobs = PathUtils.sort_jobs(jobs, ['x', 'y']) shapes = [j['shape'] for j in jobs] sims = [] numShapes = len(shapes) for ns in range(0, numShapes): profileEdgesIsOpen = False (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable if sub == 'OpenEdge': profileEdgesIsOpen = True if PathOp.FeatureStartPoint & self.opFeatures( obj) and obj.UseStartPoint: osp = obj.StartPoint self.commandlist.append( Path.Command('G0', { 'X': osp.x, 'Y': osp.y, 'F': self.horizRapid })) if ns < numShapes - 1: nextAxis = shapes[ns + 1][4] else: nextAxis = 'L' self.depthparams = self._customDepthParams(obj, strDep, finDep) try: if profileEdgesIsOpen: (pp, sim) = self._buildProfileOpenEdges( obj, shape, isHole, start, getsim) else: (pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim) except Exception as e: # pylint: disable=broad-except FreeCAD.Console.PrintError(e) FreeCAD.Console.PrintError( "Something unexpected happened. Check project and tool config." ) else: if profileEdgesIsOpen: ppCmds = pp else: ppCmds = pp.Commands if obj.EnableRotation != 'Off' and self.rotateFlag is True: # Rotate model to index for cut if axis == 'X': axisOfRot = 'A' elif axis == 'Y': axisOfRot = 'B' elif axis == 'Z': axisOfRot = 'C' else: axisOfRot = 'A' # Rotate Model to correct angle ppCmds.insert( 0, Path.Command('G0', { axisOfRot: angle, 'F': self.axialRapid })) # Raise cutter to safe height ppCmds.insert( 0, Path.Command('G0', { 'Z': obj.SafeHeight.Value, 'F': self.vertRapid })) # Return index to starting position if axis of rotation changes. if numShapes > 1: if ns != numShapes - 1: if axis != nextAxis: ppCmds.append( Path.Command('G0', { axisOfRot: 0.0, 'F': self.axialRapid })) # Eif # Save gcode commands to object command list self.commandlist.extend(ppCmds) sims.append(sim) # Eif if self.areaOpRetractTool( obj) and self.endVector is not None and len( self.commandlist) > 1: self.endVector[2] = obj.ClearanceHeight.Value self.commandlist.append( Path.Command('G0', { 'Z': obj.ClearanceHeight.Value, 'F': self.vertRapid })) # Raise cutter to safe height and rotate back to original orientation # based on next rotational operation in job if self.rotateFlag is True: resetAxis = False lastJobOp = None nextJobOp = None opIdx = 0 JOB = PathUtils.findParentJob(obj) jobOps = JOB.Operations.Group numJobOps = len(jobOps) for joi in range(0, numJobOps): jo = jobOps[joi] if jo.Name == obj.Name: opIdx = joi lastOpIdx = opIdx - 1 nextOpIdx = opIdx + 1 if lastOpIdx > -1: lastJobOp = jobOps[lastOpIdx] if nextOpIdx < numJobOps: nextJobOp = jobOps[nextOpIdx] if lastJobOp is not None: if hasattr(lastJobOp, 'EnableRotation'): PathLog.debug( 'Last Op, {}, has `EnableRotation` set to {}'.format( lastJobOp.Label, lastJobOp.EnableRotation)) if lastJobOp.EnableRotation != obj.EnableRotation: resetAxis = True # if ns == numShapes - 1: # If last shape, check next op EnableRotation setting if nextJobOp is not None: if hasattr(nextJobOp, 'EnableRotation'): PathLog.debug( 'Next Op, {}, has `EnableRotation` set to {}'.format( nextJobOp.Label, nextJobOp.EnableRotation)) if nextJobOp.EnableRotation != obj.EnableRotation: resetAxis = True # Raise to safe height if rotation activated self.commandlist.append( Path.Command('G0', { 'Z': obj.SafeHeight.Value, 'F': self.vertRapid })) # reset rotational axes if necessary if resetAxis is True: self.commandlist.append( Path.Command('G0', { 'A': 0.0, 'F': self.axialRapid })) self.commandlist.append( Path.Command('G0', { 'B': 0.0, 'F': self.axialRapid })) self.useTempJobClones( 'Delete') # Delete temp job clone group and contents self.guiMessage('title', None, show=True) # Process GUI messages to user for ton in self.tempObjectNames: # remove temporary objects by name FreeCAD.ActiveDocument.removeObject(ton) PathLog.debug("obj.Name: " + str(obj.Name) + "\n\n") return sims
def edgeForCmd(cmd, startPoint): """(cmd, startPoint). Returns an Edge representing the given command, assuming a given startPoint.""" endPoint = commandEndPoint(cmd, startPoint) if (cmd.Name in CmdMoveStraight) or (cmd.Name in CmdMoveRapid): if pointsCoincide(startPoint, endPoint): return None return Part.Edge(Part.LineSegment(startPoint, endPoint)) if cmd.Name in CmdMoveArc: center = startPoint + commandEndPoint(cmd, Vector(0,0,0), 'I', 'J', 'K') A = xy(startPoint - center) B = xy(endPoint - center) d = -B.x * A.y + B.y * A.x if isRoughly(d, 0, 0.005): PathLog.debug("Half circle arc at: (%.2f, %.2f, %.2f)" % (center.x, center.y, center.z)) # we're dealing with half a circle here angle = getAngle(A) + math.pi/2 if cmd.Name in CmdMoveCW: angle -= math.pi else: C = A + B angle = getAngle(C) PathLog.debug("Arc (%8f) at: (%.2f, %.2f, %.2f) -> angle=%f" % (d, center.x, center.y, center.z, angle / math.pi)) R = A.Length PathLog.debug("arc: p1=(%.2f, %.2f) p2=(%.2f, %.2f) -> center=(%.2f, %.2f)" % (startPoint.x, startPoint.y, endPoint.x, endPoint.y, center.x, center.y)) PathLog.debug("arc: A=(%.2f, %.2f) B=(%.2f, %.2f) -> d=%.2f" % (A.x, A.y, B.x, B.y, d)) PathLog.debug("arc: R=%.2f angle=%.2f" % (R, angle/math.pi)) if isRoughly(startPoint.z, endPoint.z): midPoint = center + Vector(math.cos(angle), math.sin(angle), 0) * R PathLog.debug("arc: (%.2f, %.2f) -> (%.2f, %.2f) -> (%.2f, %.2f)" % (startPoint.x, startPoint.y, midPoint.x, midPoint.y, endPoint.x, endPoint.y)) return Part.Edge(Part.Arc(startPoint, midPoint, endPoint)) # It's a Helix #print('angle: A=%.2f B=%.2f' % (getAngle(A)/math.pi, getAngle(B)/math.pi)) if cmd.Name in CmdMoveCW: cw = True else: cw = False angle = diffAngle(getAngle(A), getAngle(B), 'CW' if cw else 'CCW') height = endPoint.z - startPoint.z pitch = height * math.fabs(2 * math.pi / angle) if angle > 0: cw = not cw #print("Helix: R=%.2f h=%.2f angle=%.2f pitch=%.2f" % (R, height, angle/math.pi, pitch)) helix = Part.makeHelix(pitch, height, R, 0, not cw) helix.rotate(Vector(), Vector(0,0,1), 180 * getAngle(A) / math.pi) e = helix.Edges[0] helix.translate(startPoint - e.valueAt(e.FirstParameter)) return helix.Edges[0] 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 = [] finalDepths = [] 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.info(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.info( "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.info("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 break if rtn is False: if obj.AttemptInverseAngle is True and obj.InverseAngle is False: (clnBase, clnStock, angle) = self.applyInverseAngle( obj, clnBase, clnStock, axis, angle) else: PathLog.info( translate( "Path", "Consider toggling the InverseAngle property and recomputing the operation." )) 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 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, praInfo) = self.faceRotationAnalysis( obj, norm, surf) # pylint: disable=unused-variable if rtn is True: PathLog.debug( "Face not aligned after initial rotation." ) if obj.AttemptInverseAngle is True and obj.InverseAngle is False: (clnBase, clnStock, angle) = self.applyInverseAngle( obj, clnBase, clnStock, axis, angle) else: PathLog.info( translate( "Path", "Consider toggling the InverseAngle property and recomputing the operation." )) else: PathLog.debug( "Face appears to be oriented correctly." ) 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.info( 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.error(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: finDep = max(obj.FinalDepth.Value, f.BoundBox.ZMin) f.translate(FreeCAD.Vector(0, 0, finDep - 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) if obj.UseOutline: wire = TechDraw.findShapeOutline( shape, 1, FreeCAD.Vector(0, 0, 1)) wire.translate( FreeCAD.Vector( 0, 0, obj.FinalDepth.Value - wire.BoundBox.ZMin)) self.horizontal.append(Part.Face(wire)) else: self.horizontal.append(shape) for face in self.horizontal: # extrude all faces up to StartDepth and those are the removal shapes (strDep, finDep) = self.calculateStartFinalDepths( obj, face, stock) finalDepths.append(finDep) extent = FreeCAD.Vector(0, 0, strDep - finDep) self.removalshapes.append( (face.removeSplitter().extrude(extent), False, 'pathPocketShape', angle, axis, strDep, finDep)) PathLog.debug( "Extent depths are str: {}, and fin: {}".format( strDep, finDep)) # Efor face # Adjust obj.FinalDepth.Value as needed. if len(finalDepths) > 0: finalDep = min(finalDepths) if subCount == 1: obj.FinalDepth.Value = finalDep 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 getLeadEnd(self, obj, queue, action): '''returns the Gcode of LeadOut.''' # pylint: disable=unused-argument results = [] horizFeed = PathDressup.toolController(obj.Base).HorizFeed.Value R = obj.Length.Value # Radius of roll or length arcs_identical = False # Set the correct twist command if self.getDirectionOfPath(obj) == 'right': arcdir = "G2" else: arcdir = "G3" if queue[1].Name == "G1": # line p0 = queue[0].Placement.Base p1 = queue[1].Placement.Base v = self.normalize(p1.sub(p0)) else: # dealing with a circle p0 = queue[0].Placement.Base p1 = queue[1].Placement.Base v = self.normalize(p1.sub(p0)) if self.getDirectionOfPath(obj) == 'right': off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0) else: off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0) # Check if we leave at line or arc command if queue[1].Name in movecommands and queue[1].Name not in arccommands: # We have a line move vec = p1.sub(p0) vec_n = self.normalize(vec) vec_inv = self.invert(vec_n) vec_off = self.multiply(vec_inv, obj.ExtendLeadOut) #PathLog.debug("LineCMD: {}, Vxinv: {}, Vyinv: {}, Vxoff: {}, Vyoff: {}".format(queue[0].Name, vec_inv.x, vec_inv.y, vec_off.x, vec_off.y)) else: # We have an arc move pij = copy.deepcopy(p0) pij.x += queue[1].Parameters['I'] pij.y += queue[1].Parameters['J'] ve = pij.sub(p1) if arcdir == queue[1].Name: arcs_identical = True if arcdir == "G2": vec_rot = self.rotate(ve, -90) else: vec_rot = self.rotate(ve, 90) vec_n = self.normalize(vec_rot) v = vec_n if arcdir == "G3": off_v = FreeCAD.Vector(-v.y*R, v.x*R, 0.0) else: off_v = FreeCAD.Vector(v.y*R, -v.x*R, 0.0) vec_inv = self.invert(vec_rot) vec_off = self.multiply(vec_inv, obj.ExtendLeadOut) offsetvector = FreeCAD.Vector(v.x*R-vec_off.x, v.y*R-vec_off.y, 0.0) if obj.RadiusCenter == 'Radius': leadend = (p1.add(off_v)).add(offsetvector) # Rmode if arcs_identical: t = p1.sub(leadend) t = p1.add(t) leadend = t off_v = self.multiply(off_v, -1) else: leadend = p1.add(off_v) # Dmode IJ = off_v # .negative() #results.append(queue[1]) if obj.StyleOff == 'Arc': if obj.ExtendLeadOut != 0: extendcommand = Path.Command('G1', {"X": p1.x-vec_off.x, "Y": p1.y-vec_off.y, "F": horizFeed}) results.append(extendcommand) arcmove = Path.Command(arcdir, {"X": leadend.x, "Y": leadend.y, "I": IJ.x, "J": IJ.y, "F": horizFeed}) # add G2/G3 move results.append(arcmove) elif obj.StyleOff == 'Tangent': extendcommand = Path.Command('G1', {"X": leadend.x, "Y": leadend.y, "F": horizFeed}) results.append(extendcommand) else: PathLog.debug(" CURRENT_IN Perp") if obj.UseMachineCRC: # crc off results.append(Path.Command('G40', {})) return results
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)) self.removalshapes = [] 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': baseShape = oneBase[0].Shape psZMin = planeshape.BoundBox.ZMin ofst = 0.0 if obj.ClearEdges: ofst = self.tool.Diameter * 0.51 ofstShape = PathUtils.getOffsetArea(planeshape, ofst, plane=planeshape) ofstShape.translate( FreeCAD.Vector(0.0, 0.0, psZMin - ofstShape.BoundBox.ZMin)) # Calculate custom depth params for removal shape envelope, with start and final depth buffers custDepthparams = self._customDepthParams( obj, obj.StartDepth.Value + 0.2, obj.FinalDepth.Value - 0.1) # only an envelope ofstShapeEnv = PathUtils.getEnvelope(partshape=ofstShape, depthparams=custDepthparams) if obj.ExcludeRaisedAreas: env = ofstShapeEnv.cut(baseShape) env.translate(FreeCAD.Vector( 0.0, 0.0, -0.00001)) # lower removal shape into buffer zone else: env = ofstShapeEnv if holeShape: PathLog.debug("Processing holes and face ...") holeEnv = PathUtils.getEnvelope(partshape=holeShape, depthparams=self.depthparams) newEnv = env.cut(holeEnv) tup = newEnv, False, 'pathMillFace' else: PathLog.debug("Processing solid face ...") tup = env, False, 'pathMillFace' self.removalshapes.append(tup) obj.removalshape = self.removalshapes[0][0] # save removal shape return self.removalshapes