Download src/Mod/CAM/Path/Op/Profile.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 58.4 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/Profile.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Op/Profile.py
-
curl -L -o Profile.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/Profile.py
58.4 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net> * | |
| # * Copyright (c) 2016 sliptonic <shopinthewoods@gmail.com> * | |
| # * Copyright (c) 2020 Schildkroet * | |
| # * * | |
| # * This program is free software; you can redistribute it and/or modify * | |
| # * it under the terms of the GNU Lesser General Public License (LGPL) * | |
| # * as published by the Free Software Foundation; either version 2 of * | |
| # * the License, or (at your option) any later version. * | |
| # * for detail see the LICENCE text file. * | |
| # * * | |
| # * This program is distributed in the hope that it will be useful, * | |
| # * but WITHOUT ANY WARRANTY; without even the implied warranty of * | |
| # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * | |
| # * GNU Library General Public License for more details. * | |
| # * * | |
| # * You should have received a copy of the GNU Library General Public * | |
| # * License along with this program; if not, write to the Free Software * | |
| # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * | |
| # * USA * | |
| # * * | |
| # *************************************************************************** | |
| import FreeCAD | |
| import Path | |
| import Path.Base.Drillable as Drillable | |
| import Path.Op.Area as PathAreaOp | |
| import Path.Op.Base as PathOp | |
| import PathScripts.PathUtils as PathUtils | |
| import math | |
| import numpy | |
| from PySide.QtCore import QT_TRANSLATE_NOOP | |
| # lazily loaded modules | |
| from lazy_loader.lazy_loader import LazyLoader | |
| Part = LazyLoader("Part", globals(), "Part") | |
| DraftGeomUtils = LazyLoader("DraftGeomUtils", globals(), "DraftGeomUtils") | |
| translate = FreeCAD.Qt.translate | |
| __title__ = "CAM Profile Operation" | |
| __author__ = "sliptonic (Brad Collette)" | |
| __url__ = "https://www.freecad.org" | |
| __doc__ = "Create a profile toolpath based on entire model, selected faces or selected edges." | |
| __contributors__ = "Schildkroet" | |
| if False: | |
| Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) | |
| Path.Log.trackModule(Path.Log.thisModule()) | |
| else: | |
| Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) | |
| class ObjectProfile(PathAreaOp.ObjectOp): | |
| """Proxy object for Profile operations based on faces.""" | |
| def areaOpFeatures(self, obj): | |
| """areaOpFeatures(obj) ... returns operation-specific features""" | |
| return PathOp.FeatureBaseFaces | PathOp.FeatureBaseEdges | |
| def initAreaOp(self, obj): | |
| """initAreaOp(obj) ... creates all profile specific properties.""" | |
| self.propertiesReady = False | |
| self.initAreaOpProperties(obj) | |
| obj.setEditorMode("MiterLimit", 2) | |
| obj.setEditorMode("JoinType", 2) | |
| def initAreaOpProperties(self, obj, warn=False): | |
| """initAreaOpProperties(obj) ... create operation specific properties""" | |
| self.addNewProps = [] | |
| for propertytype, propertyname, grp, tt in self.areaOpProperties(): | |
| if not hasattr(obj, propertyname): | |
| obj.addProperty(propertytype, propertyname, grp, tt) | |
| self.addNewProps.append(propertyname) | |
| if len(self.addNewProps) > 0: | |
| # Set enumeration lists for enumeration properties | |
| ENUMS = self.areaOpPropertyEnumerations() | |
| for n in ENUMS: | |
| if n[0] in self.addNewProps: | |
| setattr(obj, n[0], n[1]) | |
| if warn: | |
| newPropMsg = "New property added to" | |
| newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + ". " | |
| newPropMsg += "Check its default value." + "\n" | |
| FreeCAD.Console.PrintWarning(newPropMsg) | |
| self.propertiesReady = True | |
| def areaOpProperties(self): | |
| """areaOpProperties(obj) ... returns a tuples. | |
| Each tuple contains property declaration information in the | |
| form of (prototype, name, section, tooltip).""" | |
| return [ | |
| ( | |
| "App::PropertyEnumeration", | |
| "Direction", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)", | |
| ), | |
| ), | |
| ( | |
| "App::PropertyEnumeration", | |
| "HandleMultipleFeatures", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Choose how to process multiple Base Geometry features.", | |
| ), | |
| ), | |
| ( | |
| "App::PropertyEnumeration", | |
| "JoinType", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Controls how tool moves around corners. Default=Round", | |
| ), | |
| ), | |
| ( | |
| "App::PropertyFloat", | |
| "MiterLimit", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", "Maximum distance before a miter joint is truncated" | |
| ), | |
| ), | |
| ( | |
| "App::PropertyDistance", | |
| "OffsetExtra", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "Extra value to stay away from final profile- good for roughing toolpath", | |
| ), | |
| ), | |
| ( | |
| "App::PropertyBool", | |
| "processHoles", | |
| "Profile", | |
| QT_TRANSLATE_NOOP("App::Property", "Profile holes as well as the outline"), | |
| ), | |
| ( | |
| "App::PropertyBool", | |
| "processPerimeter", | |
| "Profile", | |
| QT_TRANSLATE_NOOP("App::Property", "Profile the outline"), | |
| ), | |
| ( | |
| "App::PropertyBool", | |
| "processCircles", | |
| "Profile", | |
| QT_TRANSLATE_NOOP("App::Property", "Profile round holes"), | |
| ), | |
| ( | |
| "App::PropertyEnumeration", | |
| "Side", | |
| "Profile", | |
| QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut"), | |
| ), | |
| ( | |
| "App::PropertyBool", | |
| "UseComp", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", "Make True, if using Cutter Radius Compensation" | |
| ), | |
| ), | |
| ( | |
| "App::PropertyIntegerConstraint", | |
| "NumPasses", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "The number of passes to do. If more than one, requires a non-zero value for Stepover", | |
| ), | |
| ), | |
| ( | |
| "App::PropertyDistance", | |
| "Stepover", | |
| "Profile", | |
| QT_TRANSLATE_NOOP( | |
| "App::Property", | |
| "If doing multiple passes, the extra offset of each additional pass", | |
| ), | |
| ), | |
| ] | |
| def areaOpPropertyEnumerations(self, dataType="data"): | |
| """opPropertyEnumerations(dataType="data")... return property enumeration lists of specified dataType. | |
| Args: | |
| dataType = 'data', 'raw', 'translated' | |
| Notes: | |
| 'data' is list of internal string literals used in code | |
| 'raw' is list of (translated_text, data_string) tuples | |
| 'translated' is list of translated string literals | |
| """ | |
| # Enumeration lists for App::PropertyEnumeration properties | |
| enums = { | |
| "Direction": [ | |
| (translate("PathProfile", "CW"), "CW"), | |
| (translate("PathProfile", "CCW"), "CCW"), | |
| ], # this is the direction that the profile runs | |
| "HandleMultipleFeatures": [ | |
| (translate("PathProfile", "Collectively"), "Collectively"), | |
| (translate("PathProfile", "Individually"), "Individually"), | |
| ], | |
| "JoinType": [ | |
| (translate("PathProfile", "Round"), "Round"), | |
| (translate("PathProfile", "Square"), "Square"), | |
| (translate("PathProfile", "Miter"), "Miter"), | |
| ], # this is the direction that the Profile runs | |
| "Side": [ | |
| (translate("PathProfile", "Outside"), "Outside"), | |
| (translate("PathProfile", "Inside"), "Inside"), | |
| ], # side of profile that cutter is on in relation to direction of profile | |
| } | |
| if dataType == "raw": | |
| return enums | |
| data = list() | |
| idx = 0 if dataType == "translated" else 1 | |
| Path.Log.debug(enums) | |
| for k, v in enumerate(enums): | |
| # data[k] = [tup[idx] for tup in v] | |
| data.append((v, [tup[idx] for tup in enums[v]])) | |
| Path.Log.debug(data) | |
| return data | |
| def areaOpPropertyDefaults(self, obj, job): | |
| """areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values | |
| for the operation's properties.""" | |
| return { | |
| "Direction": "CW", | |
| "HandleMultipleFeatures": "Collectively", | |
| "JoinType": "Round", | |
| "MiterLimit": 0.1, | |
| "OffsetExtra": 0.0, | |
| "Side": "Outside", | |
| "UseComp": True, | |
| "processCircles": False, | |
| "processHoles": False, | |
| "processPerimeter": True, | |
| "Stepover": 0, | |
| "NumPasses": (1, 1, 99999, 1), | |
| } | |
| def areaOpApplyPropertyDefaults(self, obj, job, propList): | |
| # Set standard property defaults | |
| PROP_DFLTS = self.areaOpPropertyDefaults(obj, job) | |
| for n in PROP_DFLTS: | |
| if n in propList: | |
| prop = getattr(obj, n) | |
| val = PROP_DFLTS[n] | |
| setVal = False | |
| if hasattr(prop, "Value"): | |
| if isinstance(val, int) or isinstance(val, float): | |
| setVal = True | |
| if setVal: | |
| # propVal = getattr(prop, 'Value') | |
| # Need to check if `val` below should be `propVal` commented out above | |
| setattr(prop, "Value", val) | |
| else: | |
| setattr(obj, n, val) | |
| def areaOpSetDefaultValues(self, obj, job): | |
| if self.addNewProps and self.addNewProps.__len__() > 0: | |
| self.areaOpApplyPropertyDefaults(obj, job, self.addNewProps) | |
| def setOpEditorProperties(self, obj): | |
| """setOpEditorProperties(obj, porp) ... Process operation-specific changes to properties visibility.""" | |
| fc = 2 | |
| # ml = 0 if obj.JoinType == 'Miter' else 2 | |
| side = 0 if obj.UseComp else 2 | |
| opType = self._getOperationType(obj) | |
| if opType == "Contour": | |
| side = 2 | |
| elif opType == "Face": | |
| fc = 0 | |
| elif opType == "Edge": | |
| pass | |
| obj.setEditorMode("JoinType", 2) | |
| obj.setEditorMode("MiterLimit", 2) # ml | |
| obj.setEditorMode("Side", side) | |
| obj.setEditorMode("HandleMultipleFeatures", fc) | |
| obj.setEditorMode("processCircles", fc) | |
| obj.setEditorMode("processHoles", fc) | |
| obj.setEditorMode("processPerimeter", fc) | |
| def _getOperationType(self, obj): | |
| if len(obj.Base) == 0: | |
| return "Contour" | |
| # return first geometry type selected | |
| (_, subsList) = obj.Base[0] | |
| return subsList[0][:4] | |
| def areaOpOnDocumentRestored(self, obj): | |
| self.propertiesReady = False | |
| self.initAreaOpProperties(obj, warn=True) | |
| self.areaOpSetDefaultValues(obj, PathUtils.findParentJob(obj)) | |
| self.setOpEditorProperties(obj) | |
| def areaOpOnChanged(self, obj, prop): | |
| """areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.""" | |
| if prop in ["UseComp", "JoinType", "Base"]: | |
| if hasattr(self, "propertiesReady") and self.propertiesReady: | |
| self.setOpEditorProperties(obj) | |
| def areaOpAreaParams(self, obj, isHole): | |
| """areaOpAreaParams(obj, isHole) ... returns dictionary with area parameters. | |
| Do not overwrite.""" | |
| params = {} | |
| params["Fill"] = 0 | |
| params["Coplanar"] = 0 | |
| params["SectionCount"] = -1 | |
| offset = obj.OffsetExtra.Value # 0.0 | |
| num_passes = max(1, obj.NumPasses) | |
| stepover = obj.Stepover.Value | |
| if num_passes > 1 and stepover == 0: | |
| # This check is important because C++ code has a default value for stepover | |
| # if it's 0 and extra passes are requested | |
| num_passes = 1 | |
| Path.Log.warning( | |
| "Multipass profile requires a non-zero stepover. Reducing to a single pass." | |
| ) | |
| if obj.UseComp: | |
| offset = self.radius + obj.OffsetExtra.Value | |
| if obj.Side == "Inside": | |
| offset = 0 - offset | |
| stepover = -stepover | |
| if isHole: | |
| offset = 0 - offset | |
| stepover = -stepover | |
| # Modify offset and stepover to do passes from most-offset to least | |
| offset += stepover * (num_passes - 1) | |
| stepover = -stepover | |
| params["Offset"] = offset | |
| params["ExtraPass"] = num_passes - 1 | |
| params["Stepover"] = stepover | |
| jointype = ["Round", "Square", "Miter"] | |
| params["JoinType"] = jointype.index(obj.JoinType) | |
| if obj.JoinType == "Miter": | |
| params["MiterLimit"] = obj.MiterLimit | |
| if obj.SplitArcs: | |
| params["Explode"] = True | |
| params["FitArcs"] = False | |
| return params | |
| def areaOpPathParams(self, obj, isHole): | |
| """areaOpPathParams(obj, isHole) ... returns dictionary with path parameters. | |
| Do not overwrite.""" | |
| params = {} | |
| # Reverse the direction for holes | |
| if isHole: | |
| direction = "CW" if obj.Direction == "CCW" else "CCW" | |
| else: | |
| direction = obj.Direction | |
| if direction == "CCW": | |
| params["orientation"] = 0 | |
| else: | |
| params["orientation"] = 1 | |
| offset = obj.OffsetExtra.Value | |
| if obj.UseComp: | |
| offset = self.radius + obj.OffsetExtra.Value | |
| if offset == 0.0: | |
| if direction == "CCW": | |
| params["orientation"] = 1 | |
| else: | |
| params["orientation"] = 0 | |
| if obj.NumPasses > 1: | |
| # Disable path sorting to ensure that offsets appear in order, from farthest offset to closest, on all layers | |
| params["sort_mode"] = 0 | |
| return params | |
| def areaOpUseProjection(self, obj): | |
| """areaOpUseProjection(obj) ... returns True""" | |
| return True | |
| def opUpdateDepths(self, obj): | |
| if hasattr(obj, "Base") and obj.Base.__len__() == 0: | |
| obj.OpStartDepth = obj.OpStockZMax | |
| obj.OpFinalDepth = obj.OpStockZMin | |
| def areaOpShapes(self, obj): | |
| """areaOpShapes(obj) ... returns envelope for all base shapes or wires""" | |
| shapes = [] | |
| remainingObjBaseFeatures = [] | |
| self.isDebug = True if Path.Log.getLevel(Path.Log.thisModule()) == 4 else False | |
| self.inaccessibleMsg = translate( | |
| "PathProfile", | |
| "The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.", | |
| ) | |
| self.offsetExtra = obj.OffsetExtra.Value | |
| if self.isDebug: | |
| for grpNm in ["tmpDebugGrp", "tmpDebugGrp001"]: | |
| if hasattr(FreeCAD.ActiveDocument, grpNm): | |
| for go in FreeCAD.ActiveDocument.getObject(grpNm).Group: | |
| FreeCAD.ActiveDocument.removeObject(go.Name) | |
| FreeCAD.ActiveDocument.removeObject(grpNm) | |
| self.tmpGrp = FreeCAD.ActiveDocument.addObject( | |
| "App::DocumentObjectGroup", "tmpDebugGrp" | |
| ) | |
| tmpGrpNm = self.tmpGrp.Name | |
| self.JOB = PathUtils.findParentJob(obj) | |
| if obj.UseComp: | |
| self.useComp = True | |
| self.ofstRadius = self.radius + self.offsetExtra | |
| self.commandlist.append( | |
| Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")") | |
| ) | |
| else: | |
| self.useComp = False | |
| self.ofstRadius = self.offsetExtra | |
| self.commandlist.append(Path.Command("(Uncompensated Tool Path)")) | |
| # Pre-process Base Geometry to process edges | |
| if ( | |
| obj.Base and len(obj.Base) > 0 | |
| ): # The user has selected subobjects from the base. Process each. | |
| shapes.extend(self._processEdges(obj, remainingObjBaseFeatures)) | |
| Path.Log.track("returned {} shapes".format(len(shapes))) | |
| Path.Log.track(remainingObjBaseFeatures) | |
| if obj.Base and len(obj.Base) > 0 and not remainingObjBaseFeatures: | |
| # Edges were already processed, or whole model targeted. | |
| Path.Log.track("remainingObjBaseFeatures is False") | |
| elif ( | |
| remainingObjBaseFeatures and len(remainingObjBaseFeatures) > 0 | |
| ): # Process remaining features after edges processed above. | |
| for base, subsList in remainingObjBaseFeatures: | |
| holes = [] | |
| faces = [] | |
| faceDepths = [] | |
| for sub in subsList: | |
| shape = getattr(base.Shape, sub) | |
| # only process faces here | |
| if isinstance(shape, Part.Face): | |
| faces.append(shape) | |
| if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face | |
| Path.Log.debug(abs(shape.normalAt(0, 0).z)) | |
| for wire in shape.Wires: | |
| if wire.hashCode() == shape.OuterWire.hashCode(): | |
| continue | |
| holes.append((base.Shape, wire)) | |
| # Add face depth to list | |
| faceDepths.append(shape.BoundBox.ZMin) | |
| else: | |
| Path.Log.track() | |
| ignoreSub = base.Name + "." + sub | |
| msg = "Found a selected object which is not a face. Ignoring:" | |
| Path.Log.warning(msg + " {}".format(ignoreSub)) | |
| for baseShape, wire in holes: | |
| cont = False | |
| f = Part.makeFace(wire, "Part::FaceMakerSimple") | |
| drillable = Drillable.isDrillable(baseShape, f, vector=None) | |
| Path.Log.debug(drillable) | |
| if obj.processCircles: | |
| if drillable: | |
| cont = True | |
| if obj.processHoles: | |
| if not drillable: | |
| cont = True | |
| if cont: | |
| shapeEnv = PathUtils.getEnvelope( | |
| baseShape, subshape=f, depthparams=self.depthparams | |
| ) | |
| if shapeEnv: | |
| self._addDebugObject("HoleShapeEnvelope", shapeEnv) | |
| tup = shapeEnv, True, "pathProfile" | |
| shapes.append(tup) | |
| if faces and obj.processPerimeter: | |
| if obj.HandleMultipleFeatures == "Collectively": | |
| custDepthparams = self.depthparams | |
| cont = True | |
| profileshape = Part.makeCompound(faces) | |
| try: | |
| shapeEnv = PathUtils.getEnvelope( | |
| profileshape, depthparams=custDepthparams | |
| ) | |
| except Exception as ee: | |
| # PathUtils.getEnvelope() failed to return an object. | |
| msg = translate("PathProfile", "Unable to create path for face(s).") | |
| Path.Log.error(msg + "\n{}".format(ee)) | |
| cont = False | |
| if cont: | |
| self._addDebugObject("CollectCutShapeEnv", shapeEnv) | |
| tup = shapeEnv, False, "pathProfile" | |
| shapes.append(tup) | |
| elif obj.HandleMultipleFeatures == "Individually": | |
| for shape in faces: | |
| custDepthparams = self.depthparams | |
| self._addDebugObject("Indiv_Shp", shape) | |
| shapeEnv = PathUtils.getEnvelope(shape, depthparams=custDepthparams) | |
| if shapeEnv: | |
| self._addDebugObject("IndivCutShapeEnv", shapeEnv) | |
| tup = shapeEnv, False, "pathProfile" | |
| shapes.append(tup) | |
| else: # Try to build targets from the job models | |
| # No base geometry selected, so treating operation like a exterior contour operation | |
| Path.Log.track() | |
| self.opUpdateDepths(obj) | |
| if 1 == len(self.model) and hasattr(self.model[0], "Proxy"): | |
| Path.Log.debug("Single model processed.") | |
| shapes.extend(self._processEachModel(obj)) | |
| else: | |
| shapes.extend(self._processEachModel(obj)) | |
| self.removalshapes = shapes | |
| Path.Log.debug("%d shapes" % len(shapes)) | |
| # Delete the temporary objects | |
| if self.isDebug: | |
| if FreeCAD.GuiUp: | |
| import FreeCADGui | |
| FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False | |
| self.tmpGrp.purgeTouched() | |
| # for shape in shapes: | |
| # Part.show(shape[0]) | |
| # print(shape) | |
| return shapes | |
| # Method to handle each model as a whole, when no faces are selected | |
| def _processEachModel(self, obj): | |
| shapeTups = [] | |
| for base in self.model: | |
| if hasattr(base, "Shape"): | |
| env = PathUtils.getEnvelope( | |
| partshape=base.Shape, subshape=None, depthparams=self.depthparams | |
| ) | |
| if env: | |
| shapeTups.append((env, False)) | |
| return shapeTups | |
| # Edges pre-processing | |
| def _processEdges(self, obj, remainingObjBaseFeatures): | |
| Path.Log.track("remainingObjBaseFeatures: {}".format(remainingObjBaseFeatures)) | |
| shapes = [] | |
| basewires = [] | |
| ezMin = None | |
| self.cutOut = self.tool.Diameter | |
| for base, subsList in obj.Base: | |
| keepFaces = [] | |
| edgelist = [] | |
| for sub in subsList: | |
| shape = getattr(base.Shape, sub) | |
| # extract and process edges | |
| if isinstance(shape, Part.Edge): | |
| edgelist.append(getattr(base.Shape, sub)) | |
| # save faces for regular processing | |
| elif isinstance(shape, Part.Face): | |
| keepFaces.append(sub) | |
| if len(edgelist) > 0: | |
| basewires.append((base, DraftGeomUtils.findWires(edgelist))) | |
| if ezMin is None or base.Shape.BoundBox.ZMin < ezMin: | |
| ezMin = base.Shape.BoundBox.ZMin | |
| if len(keepFaces) > 0: # save faces for returning and processing | |
| remainingObjBaseFeatures.append((base, keepFaces)) | |
| Path.Log.track(basewires) | |
| for base, wires in basewires: | |
| for wire in wires: | |
| if wire.isClosed(): | |
| # Attempt to profile a closed wire | |
| # f = Part.makeFace(wire, 'Part::FaceMakerSimple') | |
| # if planar error, Comment out previous line, uncomment the next two | |
| (origWire, flatWire) = self._flattenWire(obj, wire, obj.FinalDepth.Value) | |
| f = flatWire.Wires[0] | |
| if f: | |
| shapeEnv = PathUtils.getEnvelope(Part.Face(f), depthparams=self.depthparams) | |
| if shapeEnv: | |
| tup = shapeEnv, False, "pathProfile" | |
| shapes.append(tup) | |
| else: | |
| Path.Log.error(self.inaccessibleMsg) | |
| else: | |
| # Attempt open-edges profile | |
| if self.JOB.GeometryTolerance.Value == 0.0: | |
| msg = self.JOB.Label + ".GeometryTolerance = 0.0. " | |
| msg += "Please set to an acceptable value greater than zero." | |
| Path.Log.error(msg) | |
| else: | |
| flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) | |
| zDiff = math.fabs(wire.BoundBox.ZMin - obj.FinalDepth.Value) | |
| if flattened and zDiff >= self.JOB.GeometryTolerance.Value: | |
| cutWireObjs = False | |
| openEdges = [] | |
| params = self.areaOpAreaParams(obj, False) | |
| passOffsets = [ | |
| self.ofstRadius + i * abs(params["Stepover"]) | |
| for i in range(params["ExtraPass"] + 1) | |
| ][::-1] | |
| (origWire, flatWire) = flattened | |
| self._addDebugObject("FlatWire", flatWire) | |
| for po in passOffsets: | |
| self.ofstRadius = po | |
| cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) | |
| if cutShp: | |
| cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) | |
| if cutWireObjs: | |
| for cW in cutWireObjs: | |
| openEdges.append(cW) | |
| else: | |
| Path.Log.error(self.inaccessibleMsg) | |
| if openEdges: | |
| tup = openEdges, False, "OpenEdge" | |
| shapes.append(tup) | |
| else: | |
| if zDiff < self.JOB.GeometryTolerance.Value: | |
| msg = translate( | |
| "PathProfile", | |
| "Check edge selection and Final Depth requirements for profiling open edge(s).", | |
| ) | |
| Path.Log.error(msg) | |
| else: | |
| Path.Log.error(self.inaccessibleMsg) | |
| return shapes | |
| def _flattenWire(self, obj, wire, trgtDep): | |
| """_flattenWire(obj, wire)... Return a flattened version of the wire""" | |
| Path.Log.debug("_flattenWire()") | |
| wBB = wire.BoundBox | |
| if wBB.ZLength > 0.0: | |
| Path.Log.debug("Wire is not horizontally co-planar. Flattening it.") | |
| # Extrude non-horizontal wire | |
| extFwdLen = (wBB.ZLength + 2.0) * 2.0 | |
| mbbEXT = wire.extrude(FreeCAD.Vector(0, 0, extFwdLen)) | |
| # Create cross-section of shape and translate | |
| sliceZ = wire.BoundBox.ZMin + (extFwdLen / 2) | |
| crsectFaceShp = self._makeCrossSection(mbbEXT, sliceZ, trgtDep) | |
| if crsectFaceShp is not False: | |
| return (wire, crsectFaceShp) | |
| else: | |
| return False | |
| else: | |
| srtWire = Part.Wire(Part.__sortEdges__(wire.Edges)) | |
| srtWire.translate(FreeCAD.Vector(0, 0, trgtDep - srtWire.BoundBox.ZMin)) | |
| return (wire, srtWire) | |
| # Open-edges methods | |
| def _getCutAreaCrossSection(self, obj, base, origWire, flatWire): | |
| Path.Log.debug("_getCutAreaCrossSection()") | |
| # FCAD = FreeCAD.ActiveDocument | |
| tolerance = self.JOB.GeometryTolerance.Value | |
| toolDiam = 2 * self.radius # self.radius defined in PathAreaOp or PathProfileBase modules | |
| minBfr = toolDiam * 1.25 | |
| bbBfr = (self.ofstRadius * 2) * 1.25 | |
| if bbBfr < minBfr: | |
| bbBfr = minBfr | |
| # fwBB = flatWire.BoundBox | |
| wBB = origWire.BoundBox | |
| minArea = (self.ofstRadius - tolerance) ** 2 * math.pi | |
| useWire = origWire.Wires[0] | |
| numOrigEdges = len(useWire.Edges) | |
| sdv = wBB.ZMax | |
| fdv = obj.FinalDepth.Value | |
| extLenFwd = sdv - fdv | |
| if extLenFwd <= 0.0: | |
| msg = "For open edges, verify Final Depth for this operation." | |
| FreeCAD.Console.PrintError(msg + "\n") | |
| # return False | |
| extLenFwd = 0.1 | |
| WIRE = flatWire.Wires[0] | |
| numEdges = len(WIRE.Edges) | |
| # Identify first/last edges and first/last vertex on wire | |
| begE = WIRE.Edges[0] # beginning edge | |
| endE = WIRE.Edges[numEdges - 1] # ending edge | |
| blen = begE.Length | |
| elen = endE.Length | |
| Vb = begE.Vertexes[0] # first vertex of wire | |
| Ve = endE.Vertexes[1] # last vertex of wire | |
| pb = FreeCAD.Vector(Vb.X, Vb.Y, fdv) | |
| pe = FreeCAD.Vector(Ve.X, Ve.Y, fdv) | |
| # Obtain beginning point perpendicular points | |
| if blen > 0.1: | |
| bcp = begE.valueAt(begE.getParameterByLength(0.1)) # point returned 0.1 mm along edge | |
| else: | |
| bcp = FreeCAD.Vector(begE.Vertexes[1].X, begE.Vertexes[1].Y, fdv) | |
| if elen > 0.1: | |
| ecp = endE.valueAt( | |
| endE.getParameterByLength(elen - 0.1) | |
| ) # point returned 0.1 mm along edge | |
| else: | |
| ecp = FreeCAD.Vector(endE.Vertexes[1].X, endE.Vertexes[1].Y, fdv) | |
| # Create intersection tags for determining which side of wire to cut | |
| (begInt, begExt, iTAG, eTAG) = self._makeIntersectionTags(useWire, numOrigEdges, fdv) | |
| if not begInt or not begExt: | |
| return False | |
| self.iTAG = iTAG | |
| self.eTAG = eTAG | |
| # Create extended wire boundbox, and extrude | |
| extBndbox = self._makeExtendedBoundBox(wBB, bbBfr, fdv) | |
| extBndboxEXT = extBndbox.extrude(FreeCAD.Vector(0, 0, extLenFwd)) | |
| # Cut model(selected edges) from extended edges boundbox | |
| cutArea = extBndboxEXT.cut(base.Shape) | |
| self._addDebugObject("CutArea", cutArea) | |
| # Get top and bottom faces of cut area (CA), and combine faces when necessary | |
| topFc = [] | |
| botFc = [] | |
| bbZMax = cutArea.BoundBox.ZMax | |
| bbZMin = cutArea.BoundBox.ZMin | |
| for f in range(0, len(cutArea.Faces)): | |
| FcBB = cutArea.Faces[f].BoundBox | |
| if abs(FcBB.ZMax - bbZMax) < tolerance and abs(FcBB.ZMin - bbZMax) < tolerance: | |
| topFc.append(f) | |
| if abs(FcBB.ZMax - bbZMin) < tolerance and abs(FcBB.ZMin - bbZMin) < tolerance: | |
| botFc.append(f) | |
| if len(topFc) == 0: | |
| Path.Log.error("Failed to identify top faces of cut area.") | |
| return False | |
| topComp = Part.makeCompound([cutArea.Faces[f] for f in topFc]) | |
| topComp.translate( | |
| FreeCAD.Vector(0, 0, fdv - topComp.BoundBox.ZMin) | |
| ) # Translate face to final depth | |
| if len(botFc) > 1: | |
| # Path.Log.debug('len(botFc) > 1') | |
| bndboxFace = Part.Face(extBndbox.Wires[0]) | |
| tmpFace = Part.Face(extBndbox.Wires[0]) | |
| for f in botFc: | |
| Q = tmpFace.cut(cutArea.Faces[f]) | |
| tmpFace = Q | |
| botComp = bndboxFace.cut(tmpFace) | |
| else: | |
| botComp = Part.makeCompound( | |
| [cutArea.Faces[f] for f in botFc] | |
| ) # Part.makeCompound([CA.Shape.Faces[f] for f in botFc]) | |
| botComp.translate( | |
| FreeCAD.Vector(0, 0, fdv - botComp.BoundBox.ZMin) | |
| ) # Translate face to final depth | |
| # Make common of the two | |
| comFC = topComp.common(botComp) | |
| # Determine with which set of intersection tags the model intersects | |
| (cmnIntArea, cmnExtArea) = self._checkTagIntersection(iTAG, eTAG, "QRY", comFC) | |
| if cmnExtArea > cmnIntArea: | |
| Path.Log.debug("Cutting on Ext side.") | |
| self.cutSide = "E" | |
| self.cutSideTags = eTAG | |
| tagCOM = begExt.CenterOfMass | |
| else: | |
| Path.Log.debug("Cutting on Int side.") | |
| self.cutSide = "I" | |
| self.cutSideTags = iTAG | |
| tagCOM = begInt.CenterOfMass | |
| # Make two beginning style(oriented) 'L' shape stops | |
| begStop = self._makeStop("BEG", bcp, pb, "BegStop") | |
| altBegStop = self._makeStop("END", bcp, pb, "BegStop") | |
| # Identify to which style 'L' stop the beginning intersection tag is closest, | |
| # and create partner end 'L' stop geometry, and save for application later | |
| lenBS_extETag = begStop.CenterOfMass.sub(tagCOM).Length | |
| lenABS_extETag = altBegStop.CenterOfMass.sub(tagCOM).Length | |
| if lenBS_extETag < lenABS_extETag: | |
| endStop = self._makeStop("END", ecp, pe, "EndStop") | |
| pathStops = Part.makeCompound([begStop, endStop]) | |
| else: | |
| altEndStop = self._makeStop("BEG", ecp, pe, "EndStop") | |
| pathStops = Part.makeCompound([altBegStop, altEndStop]) | |
| pathStops.translate(FreeCAD.Vector(0, 0, fdv - pathStops.BoundBox.ZMin)) | |
| # Identify closed wire in cross-section that corresponds to user-selected edge(s) | |
| workShp = comFC | |
| wire = origWire | |
| WS = workShp.Wires | |
| lenWS = len(WS) | |
| wi = 0 | |
| if lenWS < 3: | |
| # fcShp = workShp | |
| pass | |
| else: | |
| wi = None | |
| for wvt in wire.Vertexes: | |
| for w in range(0, lenWS): | |
| twr = WS[w] | |
| for v in range(0, len(twr.Vertexes)): | |
| V = twr.Vertexes[v] | |
| if abs(V.X - wvt.X) < tolerance: | |
| if abs(V.Y - wvt.Y) < tolerance: | |
| # Same vertex found. This wire to be used for offset | |
| wi = w | |
| break | |
| # Efor | |
| if wi is None: | |
| Path.Log.error( | |
| "The cut area cross-section wire does not coincide with selected edge. Wires[] index is None." | |
| ) | |
| return False | |
| else: | |
| Path.Log.debug("Cross-section Wires[] index is {}.".format(wi)) | |
| nWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi].Edges)) | |
| fcShp = Part.Face(nWire) | |
| fcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin)) | |
| # Eif | |
| # verify that wire chosen is not inside the physical model | |
| if wi > 0: # and isInterior is False: | |
| Path.Log.debug("Multiple wires in cut area. First choice is not 0. Testing.") | |
| testArea = fcShp.cut(base.Shape) | |
| isReady = self._checkTagIntersection(iTAG, eTAG, self.cutSide, testArea) | |
| Path.Log.debug("isReady {}.".format(isReady)) | |
| if isReady is False: | |
| Path.Log.debug("Using wire index {}.".format(wi - 1)) | |
| pWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi - 1].Edges)) | |
| pfcShp = Part.Face(pWire) | |
| pfcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin)) | |
| workShp = pfcShp.cut(fcShp) | |
| if testArea.Area < minArea: | |
| Path.Log.debug("offset area is less than minArea of {}.".format(minArea)) | |
| Path.Log.debug("Using wire index {}.".format(wi - 1)) | |
| pWire = Part.Wire(Part.__sortEdges__(workShp.Wires[wi - 1].Edges)) | |
| pfcShp = Part.Face(pWire) | |
| pfcShp.translate(FreeCAD.Vector(0, 0, fdv - workShp.BoundBox.ZMin)) | |
| workShp = pfcShp.cut(fcShp) | |
| # Eif | |
| # Add path stops at ends of wire | |
| cutShp = workShp.cut(pathStops) | |
| self._addDebugObject("CutShape", cutShp) | |
| return cutShp | |
| def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): | |
| Path.Log.debug("_checkTagIntersection()") | |
| # Identify intersection of Common area and Interior Tags | |
| intCmn = tstObj.common(iTAG) | |
| # Identify intersection of Common area and Exterior Tags | |
| extCmn = tstObj.common(eTAG) | |
| # Calculate common intersection (solid model side, or the non-cut side) area with tags, to determine physical cut side | |
| cmnIntArea = intCmn.Area | |
| cmnExtArea = extCmn.Area | |
| if cutSide == "QRY": | |
| return (cmnIntArea, cmnExtArea) | |
| if cmnExtArea > cmnIntArea: | |
| Path.Log.debug("Cutting on Ext side.") | |
| if cutSide == "E": | |
| return True | |
| else: | |
| Path.Log.debug("Cutting on Int side.") | |
| if cutSide == "I": | |
| return True | |
| return False | |
| def _extractPathWire(self, obj, base, flatWire, cutShp): | |
| Path.Log.debug("_extractPathWire()") | |
| subLoops = [] | |
| rtnWIRES = [] | |
| osWrIdxs = [] | |
| subDistFactor = 1.0 # Raise to include sub wires at greater distance from original | |
| fdv = obj.FinalDepth.Value | |
| wire = flatWire | |
| lstVrtIdx = len(wire.Vertexes) - 1 | |
| lstVrt = wire.Vertexes[lstVrtIdx] | |
| frstVrt = wire.Vertexes[0] | |
| cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv) | |
| cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv) | |
| # Calculate offset shape, containing cut region | |
| ofstShp = self._getOffsetArea(obj, cutShp, False) | |
| # CHECK for ZERO area of offset shape | |
| try: | |
| if hasattr(ofstShp, "Area"): | |
| osArea = ofstShp.Area | |
| if osArea: # Make LGTM parser happy | |
| pass | |
| else: | |
| Path.Log.error("No area to offset shape returned.") | |
| return [] | |
| except Exception as ee: | |
| Path.Log.error("No area to offset shape returned.\n{}".format(ee)) | |
| return [] | |
| self._addDebugObject("OffsetShape", ofstShp) | |
| numOSWires = len(ofstShp.Wires) | |
| for w in range(0, numOSWires): | |
| osWrIdxs.append(w) | |
| # Identify two vertexes for dividing offset loop | |
| NEAR0 = self._findNearestVertex(ofstShp, cent0) | |
| # min0i = 0 | |
| min0 = NEAR0[0][4] | |
| for n in range(0, len(NEAR0)): | |
| N = NEAR0[n] | |
| if N[4] < min0: | |
| min0 = N[4] | |
| # min0i = n | |
| (w0, vi0, pnt0, _, _) = NEAR0[0] # min0i | |
| near0Shp = Part.makeLine(cent0, pnt0) | |
| self._addDebugObject("Near0", near0Shp) | |
| NEAR1 = self._findNearestVertex(ofstShp, cent1) | |
| # min1i = 0 | |
| min1 = NEAR1[0][4] | |
| for n in range(0, len(NEAR1)): | |
| N = NEAR1[n] | |
| if N[4] < min1: | |
| min1 = N[4] | |
| # min1i = n | |
| (w1, vi1, pnt1, _, _) = NEAR1[0] # min1i | |
| near1Shp = Part.makeLine(cent1, pnt1) | |
| self._addDebugObject("Near1", near1Shp) | |
| if w0 != w1: | |
| Path.Log.warning( | |
| "Offset wire endpoint indexes are not equal - w0, w1: {}, {}".format(w0, w1) | |
| ) | |
| # Debugging | |
| """ | |
| if self.isDebug: | |
| Path.Log.debug('min0i is {}.'.format(min0i)) | |
| Path.Log.debug('min1i is {}.'.format(min1i)) | |
| Path.Log.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) | |
| Path.Log.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) | |
| Path.Log.debug('NEAR0 is {}.'.format(NEAR0)) | |
| Path.Log.debug('NEAR1 is {}.'.format(NEAR1)) | |
| """ | |
| mainWire = ofstShp.Wires[w0] | |
| # Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements | |
| if numOSWires > 1: | |
| # check all wires for proximity(children) to intersection tags | |
| tagsComList = [] | |
| for T in self.cutSideTags.Faces: | |
| tcom = T.CenterOfMass | |
| tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0) | |
| tagsComList.append(tv) | |
| subDist = self.ofstRadius * subDistFactor | |
| for w in osWrIdxs: | |
| if w != w0: | |
| cutSub = False | |
| VTXS = ofstShp.Wires[w].Vertexes | |
| for V in VTXS: | |
| v = FreeCAD.Vector(V.X, V.Y, 0.0) | |
| for t in tagsComList: | |
| if t.sub(v).Length < subDist: | |
| cutSub = True | |
| break | |
| if cutSub is True: | |
| break | |
| if cutSub is True: | |
| sub = Part.Wire(Part.__sortEdges__(ofstShp.Wires[w].Edges)) | |
| subLoops.append(sub) | |
| # Eif | |
| # Break offset loop into two wires - one of which is the desired profile path wire. | |
| try: | |
| (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes( | |
| mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1] | |
| ) | |
| except Exception as ee: | |
| Path.Log.error("Failed to identify offset edge.\n{}".format(ee)) | |
| return False | |
| edgs0 = [] | |
| edgs1 = [] | |
| for e in edgeIdxs0: | |
| edgs0.append(mainWire.Edges[e]) | |
| for e in edgeIdxs1: | |
| edgs1.append(mainWire.Edges[e]) | |
| part0 = Part.Wire(Part.__sortEdges__(edgs0)) | |
| part1 = Part.Wire(Part.__sortEdges__(edgs1)) | |
| # Determine which part is nearest original edge(s) | |
| distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0]) | |
| distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0]) | |
| if distToPart0 < distToPart1: | |
| rtnWIRES.append(part0) | |
| else: | |
| rtnWIRES.append(part1) | |
| rtnWIRES.extend(subLoops) | |
| return rtnWIRES | |
| def _getOffsetArea(self, obj, fcShape, isHole): | |
| """Get an offset area for a shape. Wrapper around | |
| PathUtils.getOffsetArea.""" | |
| Path.Log.debug("_getOffsetArea()") | |
| JOB = PathUtils.findParentJob(obj) | |
| tolerance = JOB.GeometryTolerance.Value | |
| offset = self.ofstRadius | |
| if isHole is False: | |
| offset = 0 - offset | |
| return PathUtils.getOffsetArea(fcShape, offset, plane=fcShape, tolerance=tolerance) | |
| def _findNearestVertex(self, shape, point): | |
| Path.Log.debug("_findNearestVertex()") | |
| PT = FreeCAD.Vector(point.x, point.y, 0.0) | |
| def sortDist(tup): | |
| return tup[4] | |
| PNTS = [] | |
| for w in range(0, len(shape.Wires)): | |
| WR = shape.Wires[w] | |
| V = WR.Vertexes[0] | |
| P = FreeCAD.Vector(V.X, V.Y, 0.0) | |
| dist = P.sub(PT).Length | |
| vi = 0 | |
| pnt = P | |
| vrt = V | |
| for v in range(0, len(WR.Vertexes)): | |
| V = WR.Vertexes[v] | |
| P = FreeCAD.Vector(V.X, V.Y, 0.0) | |
| d = P.sub(PT).Length | |
| if d < dist: | |
| dist = d | |
| vi = v | |
| pnt = P | |
| vrt = V | |
| PNTS.append((w, vi, pnt, vrt, dist)) | |
| PNTS.sort(key=sortDist) | |
| return PNTS | |
| def _separateWireAtVertexes(self, wire, VV1, VV2): | |
| Path.Log.debug("_separateWireAtVertexes()") | |
| tolerance = self.JOB.GeometryTolerance.Value | |
| grps = [[], []] | |
| wireIdxs = [[], []] | |
| V1 = FreeCAD.Vector(VV1.X, VV1.Y, VV1.Z) | |
| V2 = FreeCAD.Vector(VV2.X, VV2.Y, VV2.Z) | |
| edgeCount = len(wire.Edges) | |
| FLGS = [] | |
| for e in range(0, edgeCount): | |
| FLGS.append(0) | |
| chk4 = False | |
| for e in range(0, edgeCount): | |
| v = 0 | |
| E = wire.Edges[e] | |
| fv0 = FreeCAD.Vector(E.Vertexes[0].X, E.Vertexes[0].Y, E.Vertexes[0].Z) | |
| fv1 = FreeCAD.Vector(E.Vertexes[1].X, E.Vertexes[1].Y, E.Vertexes[1].Z) | |
| if fv0.sub(V1).Length < tolerance: | |
| v = 1 | |
| if fv1.sub(V2).Length < tolerance: | |
| v += 3 | |
| chk4 = True | |
| elif fv1.sub(V1).Length < tolerance: | |
| v = 1 | |
| if fv0.sub(V2).Length < tolerance: | |
| v += 3 | |
| chk4 = True | |
| if fv0.sub(V2).Length < tolerance: | |
| v = 3 | |
| if fv1.sub(V1).Length < tolerance: | |
| v += 1 | |
| chk4 = True | |
| elif fv1.sub(V2).Length < tolerance: | |
| v = 3 | |
| if fv0.sub(V1).Length < tolerance: | |
| v += 1 | |
| chk4 = True | |
| FLGS[e] += v | |
| # Efor | |
| # Path.Log.debug('_separateWireAtVertexes() FLGS: {}'.format(FLGS)) | |
| PRE = [] | |
| POST = [] | |
| IDXS = [] | |
| IDX1 = [] | |
| IDX2 = [] | |
| for e in range(0, edgeCount): | |
| f = FLGS[e] | |
| PRE.append(f) | |
| POST.append(f) | |
| IDXS.append(e) | |
| IDX1.append(e) | |
| IDX2.append(e) | |
| PRE.extend(FLGS) | |
| PRE.extend(POST) | |
| lenFULL = len(PRE) | |
| IDXS.extend(IDX1) | |
| IDXS.extend(IDX2) | |
| if chk4 is True: | |
| # find beginning 1 edge | |
| begIdx = None | |
| for e in range(0, lenFULL): | |
| f = PRE[e] | |
| i = IDXS[e] | |
| if f == 4: | |
| begIdx = e | |
| grps[0].append(f) | |
| wireIdxs[0].append(i) | |
| break | |
| # find first 3 edge | |
| for e in range(begIdx + 1, edgeCount + begIdx): | |
| f = PRE[e] | |
| i = IDXS[e] | |
| grps[1].append(f) | |
| wireIdxs[1].append(i) | |
| else: | |
| # find beginning 1 edge | |
| begIdx = None | |
| begFlg = False | |
| for e in range(0, lenFULL): | |
| f = PRE[e] | |
| if f == 1: | |
| if not begFlg: | |
| begFlg = True | |
| else: | |
| begIdx = e | |
| break | |
| # find first 3 edge and group all first wire edges | |
| endIdx = None | |
| for e in range(begIdx, edgeCount + begIdx): | |
| f = PRE[e] | |
| i = IDXS[e] | |
| if f == 3: | |
| grps[0].append(f) | |
| wireIdxs[0].append(i) | |
| endIdx = e | |
| break | |
| else: | |
| grps[0].append(f) | |
| wireIdxs[0].append(i) | |
| # Collect remaining edges | |
| for e in range(endIdx + 1, lenFULL): | |
| f = PRE[e] | |
| i = IDXS[e] | |
| if f == 1: | |
| grps[1].append(f) | |
| wireIdxs[1].append(i) | |
| break | |
| else: | |
| wireIdxs[1].append(i) | |
| grps[1].append(f) | |
| # Efor | |
| # Eif | |
| # Debugging | |
| """ | |
| if self.isDebug: | |
| Path.Log.debug('grps[0]: {}'.format(grps[0])) | |
| Path.Log.debug('grps[1]: {}'.format(grps[1])) | |
| Path.Log.debug('wireIdxs[0]: {}'.format(wireIdxs[0])) | |
| Path.Log.debug('wireIdxs[1]: {}'.format(wireIdxs[1])) | |
| Path.Log.debug('PRE: {}'.format(PRE)) | |
| Path.Log.debug('IDXS: {}'.format(IDXS)) | |
| """ | |
| return (wireIdxs[0], wireIdxs[1]) | |
| def _makeCrossSection(self, shape, sliceZ, zHghtTrgt=False): | |
| """_makeCrossSection(shape, sliceZ, zHghtTrgt=None)... | |
| Creates cross-section objectc from shape. Translates cross-section to zHghtTrgt if available. | |
| Makes face shape from cross-section object. Returns face shape at zHghtTrgt.""" | |
| Path.Log.debug("_makeCrossSection()") | |
| # Create cross-section of shape and translate | |
| wires = [] | |
| slcs = shape.slice(FreeCAD.Vector(0, 0, 1), sliceZ) | |
| if len(slcs) > 0: | |
| for i in slcs: | |
| wires.append(i) | |
| comp = Part.Compound(wires) | |
| if zHghtTrgt is not False: | |
| comp.translate(FreeCAD.Vector(0, 0, zHghtTrgt - comp.BoundBox.ZMin)) | |
| return comp | |
| return False | |
| def _makeExtendedBoundBox(self, wBB, bbBfr, zDep): | |
| Path.Log.debug("_makeExtendedBoundBox()") | |
| p1 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMin - bbBfr, zDep) | |
| p2 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMin - bbBfr, zDep) | |
| p3 = FreeCAD.Vector(wBB.XMax + bbBfr, wBB.YMax + bbBfr, zDep) | |
| p4 = FreeCAD.Vector(wBB.XMin - bbBfr, wBB.YMax + bbBfr, zDep) | |
| L1 = Part.makeLine(p1, p2) | |
| L2 = Part.makeLine(p2, p3) | |
| L3 = Part.makeLine(p3, p4) | |
| L4 = Part.makeLine(p4, p1) | |
| return Part.Face(Part.Wire([L1, L2, L3, L4])) | |
| def _makeIntersectionTags(self, useWire, numOrigEdges, fdv): | |
| Path.Log.debug("_makeIntersectionTags()") | |
| # Create circular probe tags around perimiter of wire | |
| extTags = [] | |
| intTags = [] | |
| tagRad = self.radius / 2 | |
| tagCnt = 0 | |
| begInt = False | |
| begExt = False | |
| for e in range(0, numOrigEdges): | |
| E = useWire.Edges[e] | |
| LE = E.Length | |
| if LE > (self.radius * 2): | |
| nt = math.ceil(LE / (tagRad * math.pi)) # (tagRad * 2 * math.pi) is circumference | |
| else: | |
| nt = 4 # desired + 1 | |
| mid = LE / nt | |
| spc = self.radius / 10 | |
| for i in range(0, int(nt)): | |
| if i == 0: | |
| if e == 0: | |
| if LE > 0.2: | |
| aspc = 0.1 | |
| else: | |
| aspc = LE * 0.75 | |
| cp1 = E.valueAt(E.getParameterByLength(0)) | |
| cp2 = E.valueAt(E.getParameterByLength(aspc)) | |
| (intTObj, extTObj) = self._makeOffsetCircleTag( | |
| cp1, cp2, tagRad, fdv, "BeginEdge[{}]_".format(e) | |
| ) | |
| if intTObj and extTObj: | |
| begInt = intTObj | |
| begExt = extTObj | |
| else: | |
| d = i * mid | |
| negTestLen = d - spc | |
| if negTestLen < 0: | |
| negTestLen = d - (LE * 0.25) | |
| posTestLen = d + spc | |
| if posTestLen > LE: | |
| posTestLen = d + (LE * 0.25) | |
| cp1 = E.valueAt(E.getParameterByLength(negTestLen)) | |
| cp2 = E.valueAt(E.getParameterByLength(posTestLen)) | |
| (intTObj, extTObj) = self._makeOffsetCircleTag( | |
| cp1, cp2, tagRad, fdv, "Edge[{}]_".format(e) | |
| ) | |
| if intTObj and extTObj: | |
| tagCnt += nt | |
| intTags.append(intTObj) | |
| extTags.append(extTObj) | |
| # tagArea = math.pi * tagRad**2 * tagCnt | |
| iTAG = Part.makeCompound(intTags) | |
| eTAG = Part.makeCompound(extTags) | |
| return (begInt, begExt, iTAG, eTAG) | |
| def _makeOffsetCircleTag(self, p1, p2, cutterRad, depth, lbl, reverse=False): | |
| # Path.Log.debug('_makeOffsetCircleTag()') | |
| pb = FreeCAD.Vector(p1.x, p1.y, 0.0) | |
| pe = FreeCAD.Vector(p2.x, p2.y, 0.0) | |
| toMid = pe.sub(pb).multiply(0.5) | |
| lenToMid = toMid.Length | |
| if lenToMid == 0.0: | |
| # Probably a vertical line segment | |
| return (False, False) | |
| cutFactor = ( | |
| cutterRad / 2.1 | |
| ) / lenToMid # = 2 is tangent to wire; > 2 allows tag to overlap wire; < 2 pulls tag away from wire | |
| perpE = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(-1 * cutFactor) # exterior tag | |
| extPnt = pb.add(toMid.add(perpE)) | |
| # make exterior tag | |
| eCntr = extPnt.add(FreeCAD.Vector(0, 0, depth)) | |
| ecw = Part.Wire(Part.makeCircle((cutterRad / 2), eCntr).Edges[0]) | |
| extTag = Part.Face(ecw) | |
| # make interior tag | |
| perpI = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(cutFactor) # interior tag | |
| intPnt = pb.add(toMid.add(perpI)) | |
| iCntr = intPnt.add(FreeCAD.Vector(0, 0, depth)) | |
| icw = Part.Wire(Part.makeCircle((cutterRad / 2), iCntr).Edges[0]) | |
| intTag = Part.Face(icw) | |
| return (intTag, extTag) | |
| def _makeStop(self, sType, pA, pB, lbl): | |
| # Path.Log.debug('_makeStop()') | |
| ofstRad = self.ofstRadius | |
| extra = self.radius / 5.0 | |
| lng = 0.05 | |
| med = lng / 2.0 | |
| shrt = lng / 5.0 | |
| E = FreeCAD.Vector(pB.x, pB.y, 0) # endpoint | |
| C = FreeCAD.Vector(pA.x, pA.y, 0) # checkpoint | |
| if self.useComp is True or (self.useComp is False and self.offsetExtra != 0): | |
| # 'L' stop shape and edge map | |
| # --1-- | |
| # | | | |
| # 2 6 | |
| # | | | |
| # | ----5----| | |
| # | 4 | |
| # -----3-------| | |
| # positive dist in _makePerp2DVector() is CCW rotation | |
| p1 = E | |
| if sType == "BEG": | |
| p2 = self._makePerp2DVector(C, E, -1 * shrt) # E1 | |
| p3 = self._makePerp2DVector(p1, p2, ofstRad + lng + extra) # E2 | |
| p4 = self._makePerp2DVector(p2, p3, shrt + ofstRad + extra) # E3 | |
| p5 = self._makePerp2DVector(p3, p4, lng + extra) # E4 | |
| p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) # E5 | |
| elif sType == "END": | |
| p2 = self._makePerp2DVector(C, E, shrt) # E1 | |
| p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + lng + extra)) # E2 | |
| p4 = self._makePerp2DVector(p2, p3, -1 * (shrt + ofstRad + extra)) # E3 | |
| p5 = self._makePerp2DVector(p3, p4, -1 * (lng + extra)) # E4 | |
| p6 = self._makePerp2DVector(p4, p5, -1 * (ofstRad + extra)) # E5 | |
| p7 = E # E6 | |
| L1 = Part.makeLine(p1, p2) | |
| L2 = Part.makeLine(p2, p3) | |
| L3 = Part.makeLine(p3, p4) | |
| L4 = Part.makeLine(p4, p5) | |
| L5 = Part.makeLine(p5, p6) | |
| L6 = Part.makeLine(p6, p7) | |
| wire = Part.Wire([L1, L2, L3, L4, L5, L6]) | |
| else: | |
| # 'L' stop shape and edge map | |
| # : | |
| # |----2-------| | |
| # 3 1 | |
| # |-----4------| | |
| # positive dist in _makePerp2DVector() is CCW rotation | |
| p1 = E | |
| if sType == "BEG": | |
| p2 = self._makePerp2DVector(C, E, -1 * (shrt + abs(self.offsetExtra))) # left, shrt | |
| p3 = self._makePerp2DVector(p1, p2, shrt + abs(self.offsetExtra)) | |
| p4 = self._makePerp2DVector(p2, p3, (med + abs(self.offsetExtra))) # FIRST POINT | |
| p5 = self._makePerp2DVector(p3, p4, shrt + abs(self.offsetExtra)) # E1 SECOND | |
| elif sType == "END": | |
| p2 = self._makePerp2DVector(C, E, (shrt + abs(self.offsetExtra))) # left, shrt | |
| p3 = self._makePerp2DVector(p1, p2, -1 * (shrt + abs(self.offsetExtra))) | |
| p4 = self._makePerp2DVector( | |
| p2, p3, -1 * (med + abs(self.offsetExtra)) | |
| ) # FIRST POINT | |
| p5 = self._makePerp2DVector( | |
| p3, p4, -1 * (shrt + abs(self.offsetExtra)) | |
| ) # E1 SECOND | |
| p6 = p1 # E4 | |
| L1 = Part.makeLine(p1, p2) | |
| L2 = Part.makeLine(p2, p3) | |
| L3 = Part.makeLine(p3, p4) | |
| L4 = Part.makeLine(p4, p5) | |
| L5 = Part.makeLine(p5, p6) | |
| wire = Part.Wire([L1, L2, L3, L4, L5]) | |
| # Eif | |
| face = Part.Face(wire) | |
| self._addDebugObject(lbl, face) | |
| return face | |
| def _makePerp2DVector(self, v1, v2, dist): | |
| p1 = FreeCAD.Vector(v1.x, v1.y, 0.0) | |
| p2 = FreeCAD.Vector(v2.x, v2.y, 0.0) | |
| toEnd = p2.sub(p1) | |
| factor = dist / toEnd.Length | |
| perp = FreeCAD.Vector(-1 * toEnd.y, toEnd.x, 0.0).multiply(factor) | |
| return p1.add(toEnd.add(perp)) | |
| def _distMidToMid(self, wireA, wireB): | |
| mpA = self._findWireMidpoint(wireA) | |
| mpB = self._findWireMidpoint(wireB) | |
| return mpA.sub(mpB).Length | |
| def _findWireMidpoint(self, wire): | |
| midPnt = None | |
| dist = 0.0 | |
| wL = wire.Length | |
| midW = wL / 2 | |
| for E in Part.sortEdges(wire.Edges)[0]: | |
| elen = E.Length | |
| d_ = dist + elen | |
| if dist < midW and midW <= d_: | |
| dtm = midW - dist | |
| midPnt = E.valueAt(E.getParameterByLength(dtm)) | |
| break | |
| else: | |
| dist += elen | |
| return midPnt | |
| # Method to add temporary debug object | |
| def _addDebugObject(self, objName, objShape): | |
| if self.isDebug: | |
| newDocObj = FreeCAD.ActiveDocument.addObject("Part::Feature", "tmp_" + objName) | |
| newDocObj.Shape = objShape | |
| newDocObj.purgeTouched() | |
| self.tmpGrp.addObject(newDocObj) | |
| def SetupProperties(): | |
| setup = PathAreaOp.SetupProperties() | |
| setup.extend([tup[1] for tup in ObjectProfile.areaOpProperties(False)]) | |
| return setup | |
| def Create(name, obj=None, parentJob=None): | |
| """Create(name) ... Creates and returns a Profile based on faces operation.""" | |
| if obj is None: | |
| obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) | |
| obj.Proxy = ObjectProfile(obj, name, parentJob) | |
| return obj | |