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 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 reject(self, resetEdit=True): PathLog.track() self.preCleanup() FreeCAD.ActiveDocument.abortTransaction() if self.deleteOnReject: PathLog.info("Uncreate Job") FreeCAD.ActiveDocument.openTransaction(translate("Path_Job", "Uncreate Job")) FreeCAD.ActiveDocument.removeObject(self.obj.Name) FreeCAD.ActiveDocument.commitTransaction() self.cleanup(resetEdit) return True
def addBaseGeometry(self, selection): added = False shapes = self.obj.BaseShapes for sel in selection: if sel.Object in shapes: PathLog.notice((translate("Path", "Base shape %s already in the list")+"\n") % (sel.Object.Label)) continue if sel.Object.isDerivedFrom('Part::Part2DObject'): if sel.HasSubObjects: for sub in sel.SubElementNames: if 'Vertex' in sub: PathLog.info(translate("Path", "Ignoring vertex")) else: self.obj.Proxy.addBase(self.obj, sel.Object, sub) else: self.obj.Base = [(p,el) for p,el in self.obj.Base if p != sel.Object] shapes.append(sel.Object) self.obj.BaseShapes = shapes added = True else: base = self.super().addBaseGeometry(selection) added = added or base return added
def isVertical(obj): '''isVertical(obj) ... answer True if obj points into Z''' if type(obj) == FreeCAD.Vector: return isRoughly(obj.x, 0) and isRoughly(obj.y, 0) if obj.ShapeType == 'Face': if type(obj.Surface) == Part.Plane: return isHorizontal(obj.Surface.Axis) if type(obj.Surface) == Part.Cylinder or type(obj.Surface) == Part.Cone: return isVertical(obj.Surface.Axis) if type(obj.Surface) == Part.Sphere: return True if type(obj.Surface) == Part.SurfaceOfExtrusion: return isVertical(obj.Surface.Direction) if type(obj.Surface) == Part.SurfaceOfRevolution: return isHorizontal(obj.Surface.Direction) if type(obj.Surface) != Part.BSplineSurface: PathLog.info(translate('PathGeom', "face %s not handled, assuming not vertical") % type(obj.Surface)) return None if obj.ShapeType == 'Edge': if type(obj.Curve) == Part.Line or type(obj.Curve) == Part.LineSegment: return isVertical(obj.Vertexes[1].Point - obj.Vertexes[0].Point) if type(obj.Curve) == Part.Circle or type(obj.Curve) == Part.Ellipse: # or type(obj.Curve) == Part.BSplineCurve: return isHorizontal(obj.Curve.Axis) if type(obj.Curve) == Part.BezierCurve: # the current assumption is that a bezier curve is vertical if its end points are vertical return isVertical(obj.Curve.EndPoint - obj.Curve.StartPoint) if type(obj.Curve) != Part.BSplineCurve: PathLog.info(translate('PathGeom', "edge %s not handled, assuming not vertical") % type(obj.Curve)) return None PathLog.error(translate('PathGeom', "isVertical(%s) not supported") % obj) return None
def addBaseGeometry(self, selection): added = False shapes = self.obj.BaseShapes for sel in selection: job = PathUtils.findParentJob(self.obj) base = job.Proxy.resourceClone(job, sel.Object) if not base: PathLog.notice((translate("Path", "%s is not a Base Model object of the job %s")+"\n") % (sel.Object.Label, job.Label)) continue if base in shapes: PathLog.notice((translate("Path", "Base shape %s already in the list")+"\n") % (sel.Object.Label)) continue if base.isDerivedFrom('Part::Part2DObject'): if sel.HasSubObjects: # selectively add some elements of the drawing to the Base for sub in sel.SubElementNames: if 'Vertex' in sub: PathLog.info(translate("Path", "Ignoring vertex")) else: self.obj.Proxy.addBase(self.obj, base, sub) else: # when adding an entire shape to BaseShapes we can take its sub shapes out of Base self.obj.Base = [(p,el) for p,el in self.obj.Base if p != base] shapes.append(base) self.obj.BaseShapes = shapes added = True else: # user wants us to engrave an edge of face of a base model base = self.super().addBaseGeometry(selection) added = added or base return added
def onDocumentRestored(self, obj): features = self.opFeatures(obj) if FeatureBaseGeometry & features and 'App::PropertyLinkSubList' == obj.getTypeIdOfProperty('Base'): PathLog.info("Replacing link property with global link (%s)." % obj.State) base = obj.Base obj.removeProperty('Base') self.addBaseProperty(obj) obj.Base = base obj.touch() obj.Document.recompute() if FeatureTool & features and not hasattr(obj, 'OpToolDiameter'): self.addOpValues(obj, ['tooldia']) if FeatureStepDown & features and not hasattr(obj, 'OpStartDepth'): if PathGeom.isRoughly(obj.StepDown.Value, 1): obj.setExpression('StepDown', 'OpToolDiameter') if FeatureDepths & features and not hasattr(obj, 'OpStartDepth'): self.addOpValues(obj, ['start', 'final']) if not hasattr(obj, 'StartDepthLock') or not obj.StartDepthLock: obj.setExpression('StartDepth', 'OpStartDepth') if FeatureNoFinalDepth & features: obj.setEditorMode('OpFinalDepth', 2) elif not hasattr(obj, 'FinalDepthLock') or not obj.FinalDepthLock: obj.setExpression('FinalDepth', 'OpFinalDepth')
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 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 setup(self, obj, initial): PathLog.info("Here we go ... ") if initial: if hasattr(obj.Base, "BoneBlacklist"): # dressing up a bone dressup obj.Side = obj.Base.Side else: # otherwise dogbones are opposite of the base path's side side = Side.Right if hasattr(obj.Base, 'Side') and obj.Base.Side == 'Inside': side = Side.Left if hasattr(obj.Base, 'Directin') and obj.Base.Direction == 'CCW': side = Side.oppositeOf(side) obj.Side = side self.toolRadius = 5 tc = PathDressup.toolController(obj.Base) if tc is None or tc.ToolNumber == 0: self.toolRadius = 5 else: tool = tc.Proxy.getTool(tc) # PathUtils.getTool(obj, tc.ToolNumber) if not tool or tool.Diameter == 0: self.toolRadius = 5 else: self.toolRadius = tool.Diameter / 2 self.shapes = {} self.dbg = []
def setup(self, obj): PathLog.info("Here we go ... ") if hasattr(obj.Base, "BoneBlacklist"): # dressing up a bone dressup obj.Side = obj.Base.Side else: # otherwise dogbones are opposite of the base path's side if obj.Base.Side == Side.Left: obj.Side = Side.Right elif obj.Base.Side == Side.Right: obj.Side = Side.Left else: # This will cause an error, which is fine for now 'cause I don't know what to do here obj.Side = 'On' self.toolRadius = 5 toolLoad = obj.ToolController if toolLoad is None or toolLoad.ToolNumber == 0: self.toolRadius = 5 else: tool = toolLoad.Proxy.getTool(toolLoad) #PathUtils.getTool(obj, toolLoad.ToolNumber) if not tool or tool.Diameter == 0: self.toolRadius = 5 else: self.toolRadius = tool.Diameter / 2 self.shapes = {} self.dbg = []
def addNewTagAt(self, point, obj): if point and obj and self.obj.Proxy.pointIsOnPath(self.obj, point): PathLog.info("addNewTagAt(%.2f, %.2f)" % (point.x, point.y)) self.Positions.append(FreeCAD.Vector(point.x, point.y, 0)) self.updateTagsView() else: print("ignore new tag at %s (obj=%s, on-path=%d" % (point, obj, 0))
def editObject(self, obj): if obj: if obj == self.obj.Base: return self.openTaskPanel('Base') if obj == self.obj.Stock: return self.openTaskPanel('Stock') PathLog.info("Expected a specific object to edit - %s not recognized" % obj.Label) return self.openTaskPanel()
def exec_(self): restoreTC = self.obj.Proxy.templateAttrs(self.obj) rc = False if not self.editor.form.exec_(): PathLog.info("revert") self.obj.Proxy.setFromTemplate(self.obj, restoreTC) rc = True return rc
def setOperationProperties(self, obj, opName): PathLog.track(obj.Label, opName) try: op = _RegisteredOps[opName] for prop in op.properties(): propName = OpPropertyName(opName, prop) if hasattr(self.obj, propName): setattr(obj, prop, getattr(self.obj, propName)) except Exception as exc: PathLog.info("SetupSheet has no support for {}".format(opName))
def setupTaskPanel(self, panel): '''setupTaskPanel(panel) ... internal function to start the editor.''' self.panel = panel FreeCADGui.Control.closeDialog() FreeCADGui.Control.showDialog(panel) panel.setupUi() job = self.Object.Proxy.getJob(self.Object) if job: job.ViewObject.Proxy.setupEditVisibility(job) else: PathLog.info("did not find no job")
def edgeForCmd(cls, cmd, startPoint): """(cmd, startPoint). Returns an Edge representing the given command, assuming a given startPoint.""" endPoint = cls.commandEndPoint(cmd, startPoint) if (cmd.Name in cls.CmdMoveStraight) or (cmd.Name in cls.CmdMoveRapid): if cls.pointsCoincide(startPoint, endPoint): return None return Part.Edge(Part.LineSegment(startPoint, endPoint)) if cmd.Name in cls.CmdMoveArc: center = startPoint + cls.commandEndPoint(cmd, Vector(0,0,0), 'I', 'J', 'K') A = cls.xy(startPoint - center) B = cls.xy(endPoint - center) d = -B.x * A.y + B.y * A.x if cls.isRoughly(d, 0, 0.005): PathLog.info("Half circle arc at: (%.2f, %.2f, %.2f)" % (center.x, center.y, center.z)) # we're dealing with half a circle here angle = cls.getAngle(A) + math.pi/2 if cmd.Name in cls.CmdMoveCW: angle -= math.pi else: C = A + B angle = cls.getAngle(C) PathLog.info("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 cls.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' % (cls.getAngle(A)/math.pi, cls.getAngle(B)/math.pi)) if cmd.Name in cls.CmdMoveCW: cw = True else: cw = False angle = cls.diffAngle(cls.getAngle(A), cls.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 * cls.getAngle(A) / math.pi) e = helix.Edges[0] helix.translate(startPoint - e.valueAt(e.FirstParameter)) return helix.Edges[0] 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 sortedTags(self, tags): ordered = [] for edge in self.bottomEdges: ts = [t for t in tags if PathGeom.isRoughly(0, Part.Vertex(t.originAt(self.minZ)).distToShape(edge)[0], 0.1)] for t in sorted(ts, key=lambda t: (t.originAt(self.minZ) - edge.valueAt(edge.FirstParameter)).Length): tags.remove(t) ordered.append(t) # disable all tags that are not on the base wire. for tag in tags: PathLog.info("Tag #%d (%.2f, %.2f, %.2f) not on base wire - disabling\n" % (len(ordered), tag.x, tag.y, self.minZ)) tag.enabled = False ordered.append(tag) return ordered
def setupBaseModel(self, obj, models=None): PathLog.track(obj.Label, models) if not hasattr(obj, 'Model'): obj.addProperty("App::PropertyLink", "Model", "Base", QtCore.QT_TRANSLATE_NOOP("PathJob", "The base objects for all operations")) model = FreeCAD.ActiveDocument.addObject("App::DocumentObjectGroup", "Model") if model.ViewObject: model.ViewObject.Visibility = False if models: model.addObjects([createModelResourceClone(obj, base) for base in models]) obj.Model = model if hasattr(obj, 'Base'): PathLog.info("Converting Job.Base to new Job.Model for {}".format(obj.Label)) obj.Model.addObject(obj.Base) obj.Base = None obj.removeProperty('Base')
def opOnChanged(self, obj, prop): '''opOnChanged(obj, prop) ... base implemenation of the notification framework - do not overwrite. The base implementation takes a stab at determining Heights and Depths if the operations's Base changes. Do not overwrite, overwrite areaOpOnChanged(obj, prop) instead.''' #PathLog.track(obj.Label, prop) if prop in ['AreaParams', 'PathParams', 'removalshape']: obj.setEditorMode(prop, 2) if PathOp.FeatureBaseGeometry & self.opFeatures(obj): if prop == 'Base' and len(obj.Base) == 1: PathLog.info("opOnChanged(%s, %s)" % (obj.Label, prop)) try: (base, sub) = obj.Base[0] bb = base.Shape.BoundBox # parent boundbox subobj = base.Shape.getElement(sub[0]) fbb = subobj.BoundBox # feature boundbox obj.StartDepth = bb.ZMax obj.ClearanceHeight = bb.ZMax + 5.0 obj.SafeHeight = bb.ZMax + 3.0 if fbb.ZMax == fbb.ZMin and fbb.ZMax == bb.ZMax: # top face obj.FinalDepth = bb.ZMin elif fbb.ZMax > fbb.ZMin and fbb.ZMax == bb.ZMax: # vertical face, full cut obj.FinalDepth = fbb.ZMin elif fbb.ZMax > fbb.ZMin and fbb.ZMin > bb.ZMin: # internal vertical wall obj.FinalDepth = fbb.ZMin elif fbb.ZMax == fbb.ZMin and fbb.ZMax > bb.ZMin: # face/shelf obj.FinalDepth = fbb.ZMin else: # catch all obj.FinalDepth = bb.ZMin if hasattr(obj, 'Side'): if bb.XLength == fbb.XLength and bb.YLength == fbb.YLength: obj.Side = "Outside" else: obj.Side = "Inside" except Exception as e: PathLog.error(translate("PatArea", "Error in calculating depths: %s") % e) obj.StartDepth = 5.0 obj.ClearanceHeight = 10.0 obj.SafeHeight = 8.0 if hasattr(obj, 'Side'): obj.Side = "Outside" self.areaOpOnChanged(obj, prop)
def updateUI(self): customSelected = self.obj.Incision == Incision.Custom self.form.custom.setEnabled(customSelected) self.form.customLabel.setEnabled(customSelected) self.updateBoneList() if PathLog.getLevel(LOG_MODULE) == PathLog.Level.DEBUG: for obj in FreeCAD.ActiveDocument.Objects: if obj.Name.startswith('Shape'): FreeCAD.ActiveDocument.removeObject(obj.Name) print('object name %s' % self.obj.Name) if hasattr(self.obj.Proxy, "shapes"): PathLog.info("showing shapes attribute") for shapes in self.obj.Proxy.shapes.itervalues(): for shape in shapes: Part.show(shape) else: PathLog.info("no shapes attribute found")
def opSetDefaultValues(self, obj): '''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) instead.''' PathLog.info("opSetDefaultValues(%s)" % (obj.Label)) if PathOp.FeatureDepths & self.opFeatures(obj): try: shape = self.areaOpShapeForDepths(obj) except: shape = None if shape: bb = shape.BoundBox obj.StartDepth = bb.ZMax obj.FinalDepth = bb.ZMin if PathOp.FeatureStepDown & self.opFeatures(obj): obj.StepDown = 1.0 else: obj.StartDepth = 1.0 obj.FinalDepth = 0.0 if PathOp.FeatureStepDown & self.opFeatures(obj): obj.StepDown = 1.0 if PathOp.FeatureHeights & self.opFeatures(obj): try: shape = self.areaOpShapeForDepths(obj) except: shape = None if shape: bb = shape.BoundBox obj.ClearanceHeight = bb.ZMax + 5.0 obj.SafeHeight = bb.ZMax + 3.0 else: obj.ClearanceHeight = 10.0 obj.SafeHeight = 8.0 self.areaOpSetDefaultValues(obj)
def onDocumentRestored(self, obj): features = self.opFeatures(obj) if FeatureBaseGeometry & features and 'App::PropertyLinkSubList' == obj.getTypeIdOfProperty('Base'): PathLog.info("Replacing link property with global link (%s)." % obj.State) base = obj.Base obj.removeProperty('Base') self.addBaseProperty(obj) obj.Base = base obj.touch() obj.Document.recompute() if FeatureTool & features and not hasattr(obj, 'OpToolDiameter'): self.addOpValues(obj, ['tooldia']) if FeatureDepths & features and not hasattr(obj, 'OpStartDepth'): self.addOpValues(obj, ['start', 'final']) if FeatureNoFinalDepth & features: obj.setEditorMode('OpFinalDepth', 2) if not hasattr(obj, 'OpStockZMax'): self.addOpValues(obj, ['stockz']) self.setEditorModes(obj, features) self.opOnDocumentRestored(obj)
def areaOpShapes(self, obj): '''areaOpShapes(obj) ... return top face''' # Facing is done either against base objects holeShape = None if obj.Base: PathLog.debug("obj.Base: {}".format(obj.Base)) faces = [] holes = [] holeEnvs = [] oneBase = [obj.Base[0][0], True] sub0 = getattr(obj.Base[0][0].Shape, obj.Base[0][1][0]) minHeight = sub0.BoundBox.ZMax for b in obj.Base: for sub in b[1]: shape = getattr(b[0].Shape, sub) if isinstance(shape, Part.Face): faces.append(shape) if shape.BoundBox.ZMin < minHeight: minHeight = shape.BoundBox.ZMin if oneBase[0] is not b[0]: oneBase[1] = False if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face for wire in shape.Wires[1:]: if obj.ExcludeRaisedAreas is True: 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.error( 'The base subobject, "{0}," is not a face. Ignoring "{0}."' .format(sub)) if obj.ExcludeRaisedAreas is True 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 == True: 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 is True and oneBase[1] is True: 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 is True and oneBase[1] is True: 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) else: env = PathUtils.getEnvelope(partshape=planeshape, depthparams=self.depthparams) if holeShape is not None: PathLog.info("Processing holes...") holeEnv = PathUtils.getEnvelope(partshape=holeShape, depthparams=self.depthparams) newEnv = env.cut(holeEnv) return [(newEnv, False)] else: return [(env, False)]
def __review(self, obj): "checks the selected job for common errors" clean = True # if obj.X_Max == obj.X_Min or obj.Y_Max == obj.Y_Min: # FreeCAD.Console.PrintWarning(translate("Path_Sanity", "It appears the machine limits haven't been set. Not able to check path extents.")+"\n") if obj.PostProcessor == '': FreeCAD.Console.PrintWarning( translate("Path_Sanity", "A Postprocessor has not been selected.") + "\n") clean = False if obj.PostProcessorOutputFile == '': FreeCAD.Console.PrintWarning( translate( "Path_Sanity", "No output file is named. You'll be prompted during postprocessing." ) + "\n") clean = False for tc in obj.ToolController: PathLog.info("Checking: {}.{}".format(obj.Label, tc.Label)) clean &= self.__checkTC(tc) for op in obj.Operations.Group: PathLog.info("Checking: {}.{}".format(obj.Label, op.Label)) if isinstance(op.Proxy, PathScripts.PathProfileContour.ObjectContour): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathProfileFaces.ObjectProfile): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathProfileEdges.ObjectProfile): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathPocket.ObjectPocket): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathPocketShape.ObjectPocket): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if not any(op.Active for op in obj.Operations.Group): #no active operations FreeCAD.Console.PrintWarning( translate( "Path_Sanity", "No active operations was found. Post processing will not result in any tooling." )) clean = False if len(obj.ToolController) == 0: #need at least one active TC FreeCAD.Console.PrintWarning( translate( "Path_Sanity", "A Tool Controller was not found. Default values are used which is dangerous. Please add a Tool Controller." ) + "\n") clean = False if clean: FreeCAD.Console.PrintMessage( translate( "Path_Sanity", "No issues detected, {} has passed basic sanity check."). format(obj.Label))
def opExecute(self, obj, getsim=False): '''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() PathLog.info("\n----- opExecute() in PathAreaOp.py") # PathLog.debug("OpDepths are Start: {}, and Final: {}".format(obj.OpStartDepth.Value, obj.OpFinalDepth.Value)) # PathLog.debug("Depths are Start: {}, and Final: {}".format(obj.StartDepth.Value, obj.FinalDepth.Value)) # PathLog.debug("initOpDepths are Start: {}, and Final: {}".format(self.initOpStartDepth, self.initOpFinalDepth)) # Instantiate class variables for operation reference self.endVector = None self.rotateFlag = False self.leadIn = 2.0 # self.safOfst / 2.0 self.cloneNames = [] self.guiMsgs = [ ] # list of message tuples (title, msg) to be displayed in GUI self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox 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] (self.safOfset, self.safOfst) = opHeights[1] # Set clearnance and safe heights based upon rotation radii if obj.EnableRotation == 'A(x)': self.strDep = self.xRotRad elif obj.EnableRotation == 'B(y)': self.strDep = self.yRotRad else: self.strDep = max(self.xRotRad, self.yRotRad) self.finDep = -1 * self.strDep obj.ClearanceHeight.Value = self.strDep + self.safOfset obj.SafeHeight.Value = self.strDep + self.safOfst if self.initWithRotation is False: if obj.FinalDepth.Value == obj.OpFinalDepth.Value: obj.FinalDepth.Value = self.finDep if obj.StartDepth.Value == obj.OpStartDepth.Value: obj.StartDepth.Value = self.strDep # Create visual axises when debugging. if PathLog.getLevel(PathLog.thisModule()) == 4: self.visualAxis() else: self.strDep = obj.StartDepth.Value self.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 self.axialRapid = 360 / safeCircum * self.horizRapid # 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( 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) # list of tuples (shape, isHole, sub, angle, axis) # 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] 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( 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: 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 # 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)) return sims
def execute(self, obj): PathLog.track() 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 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 # Build preliminary comments output = "" output += "(" + 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)) # if user wants the boundbox, calculate that PathLog.info("Boundary Shape: {}".format(obj.BoundaryShape)) if obj.BoundaryShape == 'Boundbox': bb = planeshape.BoundBox bbperim = Part.makeBox(bb.XLength, bb.YLength, 1, Vector(bb.XMin, bb.YMin, bb.ZMin), Vector(0, 0, 1)) contourwire = TechDraw.findShapeOutline(bbperim, 1, Vector(0, 0, 1)) else: contourwire = TechDraw.findShapeOutline(planeshape, 1, Vector(0, 0, 1)) # pocket = Path.Area(PocketMode=4,SectionCount=-1,SectionMode=1,Stepdown=0.499) # pocket.setParams(PocketExtraOffset = obj.PassExtension.Value, ToolRadius = self.radius) # pocket.add(planeshape, op=1) # #Part.show(contourwire) # path = Path.fromShapes(pocket.getShape()) edgelist = contourwire.Edges edgelist = Part.__sortEdges__(edgelist) # use libarea to build the pattern a = area.Area() c = PathScripts.PathKurveUtils.makeAreaCurve(edgelist, 'CW') PathLog.debug(c.text()) a.append(c) a.Reorder() output += self.buildpathlibarea(obj, a) path = Path.Path(output) if len(path.Commands) == 0: FreeCAD.Console.PrintMessage(translate("PathMillFace", "The selected settings did not produce a valid path.\n")) obj.Path = path obj.ViewObject.Visibility = True
def circularHoleExecute(self, obj, holes): """circularHoleExecute(obj, holes) ... generate drill operation for each hole in holes.""" PathLog.track() machine = PathMachineState.MachineState() self.commandlist.append(Path.Command("(Begin Drilling)")) # rapid to clearance height command = Path.Command("G0", {"Z": obj.ClearanceHeight.Value}) machine.addCommand(command) self.commandlist.append(command) self.commandlist.append(Path.Command("G90")) # Absolute distance mode # Calculate offsets to add to target edge endoffset = 0.0 if obj.ExtraOffset == "Drill Tip": endoffset = PathUtils.drillTipLength(self.tool) elif obj.ExtraOffset == "2x Drill Tip": endoffset = PathUtils.drillTipLength(self.tool) * 2 # http://linuxcnc.org/docs/html/gcode/g-code.html#gcode:g98-g99 self.commandlist.append(Path.Command(obj.ReturnLevel)) holes = PathUtils.sort_locations(holes, ["x", "y"]) # This section is technical debt. The computation of the # target shapes should be factored out for re-use. # This will likely mean refactoring upstream CircularHoleBase to pass # spotshapes instead of holes. startHeight = obj.StartDepth.Value + self.job.SetupSheet.SafeHeightOffset.Value edgelist = [] for hole in holes: v1 = FreeCAD.Vector(hole["x"], hole["y"], obj.StartDepth.Value) v2 = FreeCAD.Vector(hole["x"], hole["y"], obj.FinalDepth.Value - endoffset) edgelist.append(Part.makeLine(v1, v2)) # iterate the edgelist and generate gcode for edge in edgelist: PathLog.debug(edge) # move to hole location startPoint = edge.Vertexes[0].Point command = Path.Command("G0", { "X": startPoint.x, "Y": startPoint.y }) self.commandlist.append(command) machine.addCommand(command) command = Path.Command("G0", {"Z": startHeight}) self.commandlist.append(command) machine.addCommand(command) # command = Path.Command("G1", {"Z": obj.StartDepth.Value}) # self.commandlist.append(command) # machine.addCommand(command) # Technical Debt: We are assuming the edges are aligned. # This assumption should be corrected and the necessary rotations # performed to align the edge with the Z axis for drilling # Perform drilling dwelltime = obj.DwellTime if obj.DwellEnabled else 0.0 peckdepth = obj.PeckDepth.Value if obj.PeckEnabled else 0.0 repeat = 1 # technical debt: Add a repeat property for user control try: drillcommands = generator.generate(edge, dwelltime, peckdepth, repeat, obj.RetractHeight.Value) except ValueError as e: # any targets that fail the generator are ignored PathLog.info(e) continue for command in drillcommands: self.commandlist.append(command) machine.addCommand(command) # Cancel canned drilling cycle self.commandlist.append(Path.Command("G80")) command = Path.Command("G0", {"Z": obj.SafeHeight.Value}) self.commandlist.append(command) machine.addCommand(command) # Apply feedrates to commands PathFeedRate.setFeedRate(self.commandlist, obj.ToolController)
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 = [] # ---------------------------------------------------------------------- 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) 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) # 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 PathLog.debug( translate( 'Path', "Base Geometry sub: {}".format(sub))) 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) 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) 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))) # Eif # Efor # Efor # if False: # if False: # (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) # subList = [] # for o in range(0, len(Tags)): # subList = [] # for (base, sub, tag, angle, axis, stock) in Grps[o]: # subList.append(sub) # pair = base, subList, angle, axis, stock # baseSubsTuples.append(pair) # if False: # for (bs, sb, tg, agl, ax, stk) in allTuples: # pair = bs, [sb], agl, ax, stk # baseSubsTuples.append(pair) # ---------------------------------------------------------------------- for o in baseSubsTuples: self.horiz = [] self.vert = [] 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) self.vWires = [ TechDraw.findShapeOutline(shape, 1, FreeCAD.Vector(0, 0, 1)) for shape in self.vertical ] 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 = [] 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 = [] 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: finalDepths = min(finalDepths) if subCount == 1: obj.FinalDepth.Value = finDep 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 ] stockBB = self.stock.Shape.BoundBox self.removalshapes = [] self.bodies = [] 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)) 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: shape.tessellate(0.05) # originally 0.1 if self.removalshapes: obj.removalshape = self.removalshapes[0][0] # if PathLog.getLevel(PathLog.thisModule()) != 4: # if self.delTempNameList > 0: # for tmpNm in self.tempNameList: # FreeCAD.ActiveDocument.removeObject(tmpNm) return self.removalshapes
def foldsBackOrTurns(self, chord, side): dir = chord.getDirectionOf(self) PathLog.info(" - direction = %s/%s" % (dir, side)) return dir == 'Back' or dir == side
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 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': if obj.OpFinalDepth == obj.FinalDepth: (fzmin, fzmax) = self.getMinMaxOfFaces(Faces) 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) 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 foldsBackOrTurns(self, chord, side): direction = chord.getDirectionOf(self) PathLog.info(" - direction = %s/%s" % (direction, side)) return direction == 'Back' or direction == side
def smoothChordCommands(self, bone, inChord, outChord, edge, wire, corner, smooth, color=None): if smooth == 0: PathLog.info(" No smoothing requested") return [bone.lastCommand, outChord.g1Command(bone.F)] d = 'in' refPoint = inChord.Start if smooth == Smooth.Out: d = 'out' refPoint = outChord.End if DraftGeomUtils.areColinear(inChord.asEdge(), outChord.asEdge()): PathLog.info(" straight edge %s" % d) return [outChord.g1Command(bone.F)] pivot = None pivotDistance = 0 PathLog.info("smooth: (%.2f, %.2f)-(%.2f, %.2f)" % (edge.Vertexes[0].Point.x, edge.Vertexes[0].Point.y, edge.Vertexes[1].Point.x, edge.Vertexes[1].Point.y)) for e in wire.Edges: self.dbg.append(e) if type(e.Curve) == Part.LineSegment or type(e.Curve) == Part.Line: PathLog.debug(" (%.2f, %.2f)-(%.2f, %.2f)" % (e.Vertexes[0].Point.x, e.Vertexes[0].Point.y, e.Vertexes[1].Point.x, e.Vertexes[1].Point.y)) else: PathLog.debug(" (%.2f, %.2f)^%.2f" % (e.Curve.Center.x, e.Curve.Center.y, e.Curve.Radius)) for pt in DraftGeomUtils.findIntersection(edge, e, True, findAll=True): if not PathGeom.pointsCoincide(pt, corner) and self.pointIsOnEdge(pt, e): # debugMarker(pt, "candidate-%d-%s" % (self.boneId, d), color, 0.05) PathLog.debug(" -> candidate") distance = (pt - refPoint).Length if not pivot or pivotDistance > distance: pivot = pt pivotDistance = distance else: PathLog.debug(" -> corner intersect") if pivot: # debugCircle(pivot, self.toolRadius, "pivot.%d-%s" % (self.boneId, d), color) pivotEdge = Part.Edge(Part.Circle(pivot, FreeCAD.Vector(0, 0, 1), self.toolRadius)) t1 = self.findPivotIntersection(pivot, pivotEdge, inChord.asEdge(), inChord.End, d, color) t2 = self.findPivotIntersection(pivot, pivotEdge, outChord.asEdge(), inChord.End, d, color) commands = [] if not PathGeom.pointsCoincide(t1, inChord.Start): PathLog.debug(" add lead in") commands.append(Chord(inChord.Start, t1).g1Command(bone.F)) if bone.obj.Side == Side.Left: PathLog.debug(" add g3 command") commands.append(Chord(t1, t2).g3Command(pivot, bone.F)) else: PathLog.debug(" add g2 command center=(%.2f, %.2f) -> from (%2f, %.2f) to (%.2f, %.2f" % (pivot.x, pivot.y, t1.x, t1.y, t2.x, t2.y)) commands.append(Chord(t1, t2).g2Command(pivot, bone.F)) if not PathGeom.pointsCoincide(t2, outChord.End): PathLog.debug(" add lead out") commands.append(Chord(t2, outChord.End).g1Command(bone.F)) # debugMarker(pivot, "pivot.%d-%s" % (self.boneId, d), color, 0.2) # debugMarker(t1, "pivot.%d-%s.in" % (self.boneId, d), color, 0.1) # debugMarker(t2, "pivot.%d-%s.out" % (self.boneId, d), color, 0.1) return commands PathLog.info(" no pivot found - straight command") return [inChord.g1Command(bone.F), outChord.g1Command(bone.F)]
def execute(self, obj, forReal=True): if not obj.Base: return if forReal and not obj.Base.isDerivedFrom("Path::Feature"): return if not obj.Base.Path: return if not obj.Base.Path.Commands: return self.setup(obj, False) commands = [] # the dressed commands lastChord = Chord() # the last chord lastCommand = None # the command that generated the last chord lastBone = None # track last bone for optimizations oddsAndEnds = [] # track chords that are connected to plunges - in case they form a loop boneId = 1 self.bones = [] self.locationBlacklist = set() # boneIserted = False for (i, thisCommand) in enumerate(obj.Base.Path.Commands): # if i > 14: # if lastCommand: # commands.append(lastCommand) # lastCommand = None # commands.append(thisCommand) # continue PathLog.info("%3d: %s" % (i, thisCommand)) if thisCommand.Name in movecommands: thisChord = lastChord.moveToParameters(thisCommand.Parameters) thisIsACandidate = self.canAttachDogbone(thisCommand, thisChord) if thisIsACandidate and lastCommand and self.shouldInsertDogbone(obj, lastChord, thisChord): PathLog.info(" Found bone corner") bone = Bone(boneId, obj, lastCommand, lastChord, thisChord, Smooth.InAndOut, thisCommand.Parameters.get('F')) bones = self.insertBone(bone) boneId += 1 if lastBone: PathLog.info(" removing potential path crossing") # debugMarker(thisChord.Start, "it", (1.0, 0.0, 1.0)) commands, bones = self.removePathCrossing(commands, lastBone, bone) commands.extend(bones[:-1]) lastCommand = bones[-1] lastBone = bone elif lastCommand and thisChord.isAPlungeMove(): PathLog.info(" Looking for connection in odds and ends") haveNewLastCommand = False for chord in (chord for chord in oddsAndEnds if lastChord.connectsTo(chord)): if self.shouldInsertDogbone(obj, lastChord, chord): PathLog.info(" and there is one") bone = Bone(boneId, obj, lastCommand, lastChord, chord, Smooth.In, lastCommand.Parameters.get('F')) bones = self.insertBone(bone) boneId += 1 if lastBone: PathLog.info(" removing potential path crossing") # debugMarker(chord.Start, "it", (0.0, 1.0, 1.0)) commands, bones = self.removePathCrossing(commands, lastBone, bone) commands.extend(bones[:-1]) lastCommand = bones[-1] haveNewLastCommand = True if not haveNewLastCommand: commands.append(lastCommand) lastCommand = None commands.append(thisCommand) lastBone = None elif thisIsACandidate: PathLog.info(" is a candidate, keeping for later") if lastCommand: commands.append(lastCommand) lastCommand = thisCommand lastBone = None else: PathLog.info(" nope") if lastCommand: commands.append(lastCommand) lastCommand = None commands.append(thisCommand) lastBone = None if lastChord.isAPlungeMove() and thisIsACandidate: PathLog.info(" adding to odds and ends") oddsAndEnds.append(thisChord) lastChord = thisChord else: PathLog.info(" Clean slate") if lastCommand: commands.append(lastCommand) lastCommand = None commands.append(thisCommand) lastBone = None # for cmd in commands: # PathLog.debug("cmd = '%s'" % cmd) path = Path.Path(commands) obj.Path = path
def setup(self, obj, initial): PathLog.info("Here we go ... ") if initial: if hasattr(obj.Base, "BoneBlacklist"): # dressing up a bone dressup obj.Side = obj.Base.Side else: PathLog.info("Default side = right") # otherwise dogbones are opposite of the base path's side side = Side.Right if hasattr(obj.Base, 'Side') and obj.Base.Side == 'Inside': PathLog.info("inside -> side = left") side = Side.Left else: PathLog.info("not inside -> side stays right") if hasattr(obj.Base, 'Direction') and obj.Base.Direction == 'CCW': PathLog.info("CCW -> switch sides") side = Side.oppositeOf(side) else: PathLog.info("CW -> stay on side") obj.Side = side self.toolRadius = 5 tc = PathDressup.toolController(obj.Base) if tc is None or tc.ToolNumber == 0: self.toolRadius = 5 else: tool = tc.Proxy.getTool(tc) # PathUtils.getTool(obj, tc.ToolNumber) if not tool or float(tool.Diameter) == 0: self.toolRadius = 5 else: self.toolRadius = float(tool.Diameter) / 2 self.shapes = {} self.dbg = []
def __init__(self, obj, deleteOnReject, opPage, selectionFactory): PathLog.track(obj.Label, deleteOnReject, opPage, selectionFactory) FreeCAD.ActiveDocument.openTransaction( translate("Path", "AreaOp Operation")) self.deleteOnReject = deleteOnReject self.featurePages = [] features = obj.Proxy.opFeatures(obj) opPage.features = features if PathOp.FeatureBaseGeometry & features: if hasattr(opPage, 'taskPanelBaseGeometryPage'): self.featurePages.append( opPage.taskPanelBaseGeometryPage(obj, features)) else: self.featurePages.append( TaskPanelBaseGeometryPage(obj, features)) if PathOp.FeatureLocations & features: if hasattr(opPage, 'taskPanelBaseLocationPage'): self.featurePages.append( opPage.taskPanelBaseLocationPage(obj, features)) else: self.featurePages.append( TaskPanelBaseLocationPage(obj, features)) if PathOp.FeatureDepths & features: if hasattr(opPage, 'taskPanelDepthsPage'): self.featurePages.append( opPage.taskPanelDepthsPage(obj, features)) else: self.featurePages.append(TaskPanelDepthsPage(obj, features)) if PathOp.FeatureHeights & features: if hasattr(opPage, 'taskPanelHeightsPage'): self.featurePages.append( opPage.taskPanelHeightsPage(obj, features)) else: self.featurePages.append(TaskPanelHeightsPage(obj, features)) self.featurePages.append(opPage) for page in self.featurePages: page.initPage(obj) page.onDirtyChanged(self.pageDirtyChanged) taskPanelLayout = PathPreferences.defaultTaskPanelLayout() if taskPanelLayout < 2: opTitle = opPage.getTitle(obj) opPage.setTitle(translate('PathOp', 'Operation')) toolbox = QtGui.QToolBox() if taskPanelLayout == 0: for page in self.featurePages: toolbox.addItem(page.form, page.getTitle(obj)) toolbox.setCurrentIndex(len(self.featurePages) - 1) else: for page in reversed(self.featurePages): toolbox.addItem(page.form, page.getTitle(obj)) PathLog.info("Title: '%s'" % opTitle) toolbox.setWindowTitle(opTitle) if opPage.getIcon(obj): toolbox.setWindowIcon(QtGui.QIcon(opPage.getIcon(obj))) self.form = toolbox elif taskPanelLayout == 2: forms = [] for page in self.featurePages: page.form.setWindowTitle(page.getTitle(obj)) forms.append(page.form) self.form = forms elif taskPanelLayout == 3: forms = [] for page in reversed(self.featurePages): page.form.setWindowTitle(page.getTitle(obj)) forms.append(page.form) self.form = forms self.selectionFactory = selectionFactory self.obj = obj self.isdirty = deleteOnReject
def Execute(op, obj): # pylint: disable=global-statement global sceneGraph global topZ if FreeCAD.GuiUp: sceneGraph = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph() PathLog.info("*** Adaptive toolpath processing started...\n") # hide old toolpaths during recalculation obj.Path = Path.Path("(Calculating...)") if FreeCAD.GuiUp: #store old visibility state job = op.getJob(obj) oldObjVisibility = obj.ViewObject.Visibility oldJobVisibility = job.ViewObject.Visibility obj.ViewObject.Visibility = False job.ViewObject.Visibility = False FreeCADGui.updateGui() try: helixDiameter = obj.HelixDiameterLimit.Value topZ = op.stock.Shape.BoundBox.ZMax obj.Stopped = False obj.StopProcessing = False if obj.Tolerance < 0.001: obj.Tolerance = 0.001 # Get list of working edges for adaptive algorithm pathArray = op.pathArray if not pathArray: PathLog.error("No wire data returned.") return path2d = convertTo2d(pathArray) stockPaths = [] if hasattr(op.stock, "StockType") and op.stock.StockType == "CreateCylinder": stockPaths.append([discretize(op.stock.Shape.Edges[0])]) else: stockBB = op.stock.Shape.BoundBox v = [] v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMin, 0)) v.append(FreeCAD.Vector(stockBB.XMax, stockBB.YMin, 0)) v.append(FreeCAD.Vector(stockBB.XMax, stockBB.YMax, 0)) v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMax, 0)) v.append(FreeCAD.Vector(stockBB.XMin, stockBB.YMin, 0)) stockPaths.append([v]) stockPath2d = convertTo2d(stockPaths) opType = area.AdaptiveOperationType.ClearingInside if obj.OperationType == "Clearing": if obj.Side == "Outside": opType = area.AdaptiveOperationType.ClearingOutside else: opType = area.AdaptiveOperationType.ClearingInside else: # profiling if obj.Side == "Outside": opType = area.AdaptiveOperationType.ProfilingOutside else: opType = area.AdaptiveOperationType.ProfilingInside keepToolDownRatio = 3.0 if hasattr(obj, 'KeepToolDownRatio'): keepToolDownRatio = float(obj.KeepToolDownRatio) # put here all properties that influence calculation of adaptive base paths, inputStateObject = { "tool": float(op.tool.Diameter), "tolerance": float(obj.Tolerance), "geometry": path2d, "stockGeometry": stockPath2d, "stepover": float(obj.StepOver), "effectiveHelixDiameter": float(helixDiameter), "operationType": obj.OperationType, "side": obj.Side, "forceInsideOut": obj.ForceInsideOut, "finishingProfile": obj.FinishingProfile, "keepToolDownRatio": keepToolDownRatio, "stockToLeave": float(obj.StockToLeave) } inputStateChanged = False adaptiveResults = None if obj.AdaptiveOutputState is not None and obj.AdaptiveOutputState != "": adaptiveResults = obj.AdaptiveOutputState if json.dumps(obj.AdaptiveInputState) != json.dumps(inputStateObject): inputStateChanged = True adaptiveResults = None # progress callback fn, if return true it will stop processing def progressFn(tpaths): if FreeCAD.GuiUp: for path in tpaths: #path[0] contains the MotionType, #path[1] contains list of points if path[0] == area.AdaptiveMotionType.Cutting: sceneDrawPath(path[1],(0,0,1)) else: sceneDrawPath(path[1],(1,0,1)) FreeCADGui.updateGui() return obj.StopProcessing start = time.time() if inputStateChanged or adaptiveResults is None: a2d = area.Adaptive2d() a2d.stepOverFactor = 0.01 * obj.StepOver a2d.toolDiameter = float(op.tool.Diameter) a2d.helixRampDiameter = helixDiameter a2d.keepToolDownDistRatio = keepToolDownRatio a2d.stockToLeave = float(obj.StockToLeave) a2d.tolerance = float(obj.Tolerance) a2d.forceInsideOut = obj.ForceInsideOut a2d.finishingProfile = obj.FinishingProfile a2d.opType = opType # EXECUTE results = a2d.Execute(stockPath2d, path2d, progressFn) # need to convert results to python object to be JSON serializable adaptiveResults = [] for result in results: adaptiveResults.append({ "HelixCenterPoint": result.HelixCenterPoint, "StartPoint": result.StartPoint, "AdaptivePaths": result.AdaptivePaths, "ReturnMotionType": result.ReturnMotionType}) # GENERATE GenerateGCode(op, obj, adaptiveResults, helixDiameter) if not obj.StopProcessing: PathLog.info("*** Done. Elapsed time: %f sec\n\n" % (time.time()-start)) obj.AdaptiveOutputState = adaptiveResults obj.AdaptiveInputState = inputStateObject else: PathLog.info("*** Processing cancelled (after: %f sec).\n\n" % (time.time()-start)) finally: if FreeCAD.GuiUp: obj.ViewObject.Visibility = oldObjVisibility job.ViewObject.Visibility = oldJobVisibility sceneClean()
def __init__(self, obj, deleteOnReject, opPage, selectionFactory): PathLog.track(obj.Label, deleteOnReject, opPage, selectionFactory) FreeCAD.ActiveDocument.openTransaction(translate("Path", "AreaOp Operation")) self.deleteOnReject = deleteOnReject self.featurePages = [] features = obj.Proxy.opFeatures(obj) opPage.features = features if PathOp.FeatureBaseGeometry & features: if hasattr(opPage, 'taskPanelBaseGeometryPage'): self.featurePages.append(opPage.taskPanelBaseGeometryPage(obj, features)) else: self.featurePages.append(TaskPanelBaseGeometryPage(obj, features)) if PathOp.FeatureLocations & features: if hasattr(opPage, 'taskPanelBaseLocationPage'): self.featurePages.append(opPage.taskPanelBaseLocationPage(obj, features)) else: self.featurePages.append(TaskPanelBaseLocationPage(obj, features)) if PathOp.FeatureDepths & features or PathOp.FeatureStepDown: if hasattr(opPage, 'taskPanelDepthsPage'): self.featurePages.append(opPage.taskPanelDepthsPage(obj, features)) else: self.featurePages.append(TaskPanelDepthsPage(obj, features)) if PathOp.FeatureHeights & features: if hasattr(opPage, 'taskPanelHeightsPage'): self.featurePages.append(opPage.taskPanelHeightsPage(obj, features)) else: self.featurePages.append(TaskPanelHeightsPage(obj, features)) self.featurePages.append(opPage) for page in self.featurePages: page.initPage(obj) page.onDirtyChanged(self.pageDirtyChanged) taskPanelLayout = PathPreferences.defaultTaskPanelLayout() if taskPanelLayout < 2: opTitle = opPage.getTitle(obj) opPage.setTitle(translate('PathOp', 'Operation')) toolbox = QtGui.QToolBox() if taskPanelLayout == 0: for page in self.featurePages: toolbox.addItem(page.form, page.getTitle(obj)) toolbox.setCurrentIndex(len(self.featurePages)-1) else: for page in reversed(self.featurePages): toolbox.addItem(page.form, page.getTitle(obj)) PathLog.info("Title: '%s'" % opTitle) toolbox.setWindowTitle(opTitle) if opPage.getIcon(obj): toolbox.setWindowIcon(QtGui.QIcon(opPage.getIcon(obj))) self.form = toolbox elif taskPanelLayout == 2: forms = [] for page in self.featurePages: page.form.setWindowTitle(page.getTitle(obj)) forms.append(page.form) self.form = forms elif taskPanelLayout == 3: forms = [] for page in reversed(self.featurePages): page.form.setWindowTitle(page.getTitle(obj)) forms.append(page.form) self.form = forms self.selectionFactory = selectionFactory self.obj = obj self.isdirty = deleteOnReject
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 __review(self, obj): "checks the selected job for common errors" clean = True # if obj.X_Max == obj.X_Min or obj.Y_Max == obj.Y_Min: # FreeCAD.Console.PrintWarning(translate("Path_Sanity", "It appears the machine limits haven't been set. Not able to check path extents.")+"\n") if obj.PostProcessor == '': FreeCAD.Console.PrintWarning(translate("Path_Sanity", "A Postprocessor has not been selected.")+"\n") clean = False if obj.PostProcessorOutputFile == '': FreeCAD.Console.PrintWarning(translate("Path_Sanity", "No output file is named. You'll be prompted during postprocessing.")+"\n") clean = False for tc in obj.ToolController: PathLog.info("Checking: {}.{}".format(obj.Label, tc.Label)) clean &= self.__checkTC(tc) for op in obj.Operations.Group: PathLog.info("Checking: {}.{}".format(obj.Label, op.Label)) if isinstance(op.Proxy, PathScripts.PathProfileContour.ObjectContour): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathProfileFaces.ObjectProfile): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathProfileEdges.ObjectProfile): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathPocket.ObjectPocket): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if isinstance(op.Proxy, PathScripts.PathPocketShape.ObjectPocket): if op.Active: # simobj = op.Proxy.execute(op, getsim=True) # if simobj is not None: # print ('collision detected') # PC.getCollisionObject(self.baseobj, simobj) # clean = False pass if not any(op.Active for op in obj.Operations.Group): #no active operations FreeCAD.Console.PrintWarning(translate("Path_Sanity", "No active operations was found. Post processing will not result in any tooling.")) clean = False if len(obj.ToolController) == 0: #need at least one active TC FreeCAD.Console.PrintWarning(translate("Path_Sanity", "A Tool Controller was not found. Default values are used which is dangerous. Please add a Tool Controller.")+"\n") clean = False if clean: FreeCAD.Console.PrintMessage(translate("Path_Sanity", "No issues detected, {} has passed basic sanity check.").format(obj.Label))
def combineHorizontalFaces(faces): '''combineHorizontalFaces(faces)... This function successfully identifies and combines multiple connected faces and works on multiple independent faces with multiple connected faces within the list. The return value is list of simplifed faces. The Adaptive op is not concerned with which hole edges belong to which face. Attempts to do the same shape connecting failed with TechDraw.findShapeOutline() and PathGeom.combineConnectedShapes(), so this algorithm was created. ''' horizontal = list() offset = 10.0 topFace = None innerFaces = list() # Verify all incoming faces are at Z=0.0 for f in faces: if f.BoundBox.ZMin != 0.0: f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin)) # Make offset compound boundbox solid and cut incoming face extrusions from it allFaces = Part.makeCompound(faces) if hasattr(allFaces, "Area") and isRoughly(allFaces.Area, 0.0): msg = translate( 'PathGeom', 'Zero working area to process. Check your selection and settings.') PathLog.info(msg) return horizontal afbb = allFaces.BoundBox bboxFace = makeBoundBoxFace(afbb, offset, -5.0) bboxSolid = bboxFace.extrude(FreeCAD.Vector(0.0, 0.0, 10.0)) extrudedFaces = list() for f in faces: extrudedFaces.append(f.extrude(FreeCAD.Vector(0.0, 0.0, 6.0))) # Fuse all extruded faces together allFacesSolid = extrudedFaces.pop() for i in range(len(extrudedFaces)): temp = extrudedFaces.pop().fuse(allFacesSolid) allFacesSolid = temp cut = bboxSolid.cut(allFacesSolid) # Debug # Part.show(cut) # FreeCAD.ActiveDocument.ActiveObject.Label = "cut" # Identify top face and floating inner faces that are the holes in incoming faces for f in cut.Faces: fbb = f.BoundBox if isRoughly(fbb.ZMin, 5.0) and isRoughly(fbb.ZMax, 5.0): if (isRoughly(afbb.XMin - offset, fbb.XMin) and isRoughly(afbb.XMax + offset, fbb.XMax) and isRoughly(afbb.YMin - offset, fbb.YMin) and isRoughly(afbb.YMax + offset, fbb.YMax)): topFace = f else: innerFaces.append(f) if not topFace: return horizontal outer = [Part.Face(w) for w in topFace.Wires[1:]] if outer: for f in outer: f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin)) if innerFaces: # inner = [Part.Face(f.Wire1) for f in innerFaces] inner = innerFaces for f in inner: f.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - f.BoundBox.ZMin)) innerComp = Part.makeCompound(inner) outerComp = Part.makeCompound(outer) cut = outerComp.cut(innerComp) for f in cut.Faces: horizontal.append(f) else: horizontal = outer return horizontal
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 addToolController(self, tc): group = self.obj.ToolController PathLog.info("addToolController(%s): %s" % (tc.Label, [t.Label for t in group])) if tc.Name not in [str(t.Name) for t in group]: group.append(tc) self.obj.ToolController = group