| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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 |
| |
|
| | |
| | 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: |
| | |
| | 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", |
| | ), |
| | ), |
| | ] |
| |
|
| | @classmethod |
| | 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 |
| | """ |
| |
|
| | |
| | enums = { |
| | "Direction": [ |
| | (translate("PathProfile", "CW"), "CW"), |
| | (translate("PathProfile", "CCW"), "CCW"), |
| | ], |
| | "HandleMultipleFeatures": [ |
| | (translate("PathProfile", "Collectively"), "Collectively"), |
| | (translate("PathProfile", "Individually"), "Individually"), |
| | ], |
| | "JoinType": [ |
| | (translate("PathProfile", "Round"), "Round"), |
| | (translate("PathProfile", "Square"), "Square"), |
| | (translate("PathProfile", "Miter"), "Miter"), |
| | ], |
| | "Side": [ |
| | (translate("PathProfile", "Outside"), "Outside"), |
| | (translate("PathProfile", "Inside"), "Inside"), |
| | ], |
| | } |
| |
|
| | 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.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): |
| | |
| | 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: |
| | |
| | |
| | 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 |
| | |
| | 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) |
| | 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" |
| |
|
| | |
| | (_, 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 |
| | num_passes = max(1, obj.NumPasses) |
| | stepover = obj.Stepover.Value |
| | if num_passes > 1 and stepover == 0: |
| | |
| | |
| | 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 |
| |
|
| | |
| | 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 = {} |
| |
|
| | |
| | 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: |
| | |
| | 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)")) |
| |
|
| | |
| | if ( |
| | obj.Base and len(obj.Base) > 0 |
| | ): |
| | 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: |
| | |
| | Path.Log.track("remainingObjBaseFeatures is False") |
| | elif ( |
| | remainingObjBaseFeatures and len(remainingObjBaseFeatures) > 0 |
| | ): |
| | for base, subsList in remainingObjBaseFeatures: |
| | holes = [] |
| | faces = [] |
| | faceDepths = [] |
| |
|
| | for sub in subsList: |
| | shape = getattr(base.Shape, sub) |
| | |
| | if isinstance(shape, Part.Face): |
| | faces.append(shape) |
| | if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): |
| | 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)) |
| |
|
| | |
| | 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: |
| | |
| | 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: |
| | |
| | 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)) |
| |
|
| | |
| | if self.isDebug: |
| | if FreeCAD.GuiUp: |
| | import FreeCADGui |
| |
|
| | FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False |
| | self.tmpGrp.purgeTouched() |
| |
|
| | |
| | |
| | |
| | return shapes |
| |
|
| | |
| | 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 |
| |
|
| | |
| | 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) |
| | |
| | if isinstance(shape, Part.Edge): |
| | edgelist.append(getattr(base.Shape, sub)) |
| | |
| | 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: |
| | remainingObjBaseFeatures.append((base, keepFaces)) |
| |
|
| | Path.Log.track(basewires) |
| | for base, wires in basewires: |
| | for wire in wires: |
| | if wire.isClosed(): |
| | |
| |
|
| | |
| | |
| | (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: |
| | |
| | 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.") |
| |
|
| | |
| | extFwdLen = (wBB.ZLength + 2.0) * 2.0 |
| | mbbEXT = wire.extrude(FreeCAD.Vector(0, 0, extFwdLen)) |
| |
|
| | |
| | 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) |
| |
|
| | |
| | def _getCutAreaCrossSection(self, obj, base, origWire, flatWire): |
| | Path.Log.debug("_getCutAreaCrossSection()") |
| | |
| | tolerance = self.JOB.GeometryTolerance.Value |
| | toolDiam = 2 * self.radius |
| | minBfr = toolDiam * 1.25 |
| | bbBfr = (self.ofstRadius * 2) * 1.25 |
| | if bbBfr < minBfr: |
| | bbBfr = minBfr |
| | |
| | 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") |
| | |
| | extLenFwd = 0.1 |
| | WIRE = flatWire.Wires[0] |
| | numEdges = len(WIRE.Edges) |
| |
|
| | |
| | begE = WIRE.Edges[0] |
| | endE = WIRE.Edges[numEdges - 1] |
| | blen = begE.Length |
| | elen = endE.Length |
| | Vb = begE.Vertexes[0] |
| | Ve = endE.Vertexes[1] |
| | pb = FreeCAD.Vector(Vb.X, Vb.Y, fdv) |
| | pe = FreeCAD.Vector(Ve.X, Ve.Y, fdv) |
| |
|
| | |
| | if blen > 0.1: |
| | bcp = begE.valueAt(begE.getParameterByLength(0.1)) |
| | 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) |
| | ) |
| | else: |
| | ecp = FreeCAD.Vector(endE.Vertexes[1].X, endE.Vertexes[1].Y, fdv) |
| |
|
| | |
| | (begInt, begExt, iTAG, eTAG) = self._makeIntersectionTags(useWire, numOrigEdges, fdv) |
| | if not begInt or not begExt: |
| | return False |
| | self.iTAG = iTAG |
| | self.eTAG = eTAG |
| |
|
| | |
| | extBndbox = self._makeExtendedBoundBox(wBB, bbBfr, fdv) |
| | extBndboxEXT = extBndbox.extrude(FreeCAD.Vector(0, 0, extLenFwd)) |
| |
|
| | |
| | cutArea = extBndboxEXT.cut(base.Shape) |
| | self._addDebugObject("CutArea", cutArea) |
| |
|
| | |
| | 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) |
| | ) |
| | if 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] |
| | ) |
| | botComp.translate( |
| | FreeCAD.Vector(0, 0, fdv - botComp.BoundBox.ZMin) |
| | ) |
| |
|
| | |
| | comFC = topComp.common(botComp) |
| |
|
| | |
| | (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 |
| |
|
| | |
| | begStop = self._makeStop("BEG", bcp, pb, "BegStop") |
| | altBegStop = self._makeStop("END", bcp, pb, "BegStop") |
| |
|
| | |
| | |
| | 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)) |
| |
|
| | |
| | workShp = comFC |
| | wire = origWire |
| | WS = workShp.Wires |
| | lenWS = len(WS) |
| | wi = 0 |
| | if lenWS < 3: |
| | |
| | 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: |
| | |
| | wi = w |
| | break |
| | |
| |
|
| | 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)) |
| | |
| |
|
| | |
| | if wi > 0: |
| | 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) |
| | |
| |
|
| | |
| | cutShp = workShp.cut(pathStops) |
| | self._addDebugObject("CutShape", cutShp) |
| |
|
| | return cutShp |
| |
|
| | def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): |
| | Path.Log.debug("_checkTagIntersection()") |
| | |
| | intCmn = tstObj.common(iTAG) |
| |
|
| | |
| | extCmn = tstObj.common(eTAG) |
| |
|
| | |
| | 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 |
| | 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) |
| |
|
| | |
| | ofstShp = self._getOffsetArea(obj, cutShp, False) |
| |
|
| | |
| | try: |
| | if hasattr(ofstShp, "Area"): |
| | osArea = ofstShp.Area |
| | if osArea: |
| | 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) |
| |
|
| | |
| | NEAR0 = self._findNearestVertex(ofstShp, cent0) |
| | |
| | min0 = NEAR0[0][4] |
| | for n in range(0, len(NEAR0)): |
| | N = NEAR0[n] |
| | if N[4] < min0: |
| | min0 = N[4] |
| | |
| | (w0, vi0, pnt0, _, _) = NEAR0[0] |
| | near0Shp = Part.makeLine(cent0, pnt0) |
| | self._addDebugObject("Near0", near0Shp) |
| |
|
| | NEAR1 = self._findNearestVertex(ofstShp, cent1) |
| | |
| | min1 = NEAR1[0][4] |
| | for n in range(0, len(NEAR1)): |
| | N = NEAR1[n] |
| | if N[4] < min1: |
| | min1 = N[4] |
| | |
| | (w1, vi1, pnt1, _, _) = NEAR1[0] |
| | 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) |
| | ) |
| |
|
| | |
| | """ |
| | 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] |
| |
|
| | |
| | if numOSWires > 1: |
| | |
| | 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) |
| | |
| |
|
| | |
| | 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)) |
| |
|
| | |
| | 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 |
| | |
| |
|
| | |
| |
|
| | 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: |
| | |
| | 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 |
| | |
| | for e in range(begIdx + 1, edgeCount + begIdx): |
| | f = PRE[e] |
| | i = IDXS[e] |
| | grps[1].append(f) |
| | wireIdxs[1].append(i) |
| | else: |
| | |
| | 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 |
| | |
| | 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) |
| | |
| | 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) |
| | |
| | |
| |
|
| | |
| | """ |
| | 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()") |
| | |
| | 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()") |
| | |
| | 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)) |
| | else: |
| | nt = 4 |
| | 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) |
| | |
| | iTAG = Part.makeCompound(intTags) |
| | eTAG = Part.makeCompound(extTags) |
| |
|
| | return (begInt, begExt, iTAG, eTAG) |
| |
|
| | def _makeOffsetCircleTag(self, p1, p2, cutterRad, depth, lbl, reverse=False): |
| | |
| | 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: |
| | |
| | return (False, False) |
| |
|
| | cutFactor = ( |
| | cutterRad / 2.1 |
| | ) / lenToMid |
| | perpE = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(-1 * cutFactor) |
| | extPnt = pb.add(toMid.add(perpE)) |
| |
|
| | |
| | eCntr = extPnt.add(FreeCAD.Vector(0, 0, depth)) |
| | ecw = Part.Wire(Part.makeCircle((cutterRad / 2), eCntr).Edges[0]) |
| | extTag = Part.Face(ecw) |
| |
|
| | |
| | perpI = FreeCAD.Vector(-1 * toMid.y, toMid.x, 0.0).multiply(cutFactor) |
| | 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): |
| | |
| | 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) |
| | C = FreeCAD.Vector(pA.x, pA.y, 0) |
| |
|
| | if self.useComp is True or (self.useComp is False and self.offsetExtra != 0): |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | p1 = E |
| | if sType == "BEG": |
| | p2 = self._makePerp2DVector(C, E, -1 * shrt) |
| | p3 = self._makePerp2DVector(p1, p2, ofstRad + lng + extra) |
| | p4 = self._makePerp2DVector(p2, p3, shrt + ofstRad + extra) |
| | p5 = self._makePerp2DVector(p3, p4, lng + extra) |
| | p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) |
| | elif sType == "END": |
| | p2 = self._makePerp2DVector(C, E, shrt) |
| | p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + lng + extra)) |
| | p4 = self._makePerp2DVector(p2, p3, -1 * (shrt + ofstRad + extra)) |
| | p5 = self._makePerp2DVector(p3, p4, -1 * (lng + extra)) |
| | p6 = self._makePerp2DVector(p4, p5, -1 * (ofstRad + extra)) |
| | p7 = E |
| | 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: |
| | |
| | |
| | |
| | |
| | |
| | |
| | p1 = E |
| | if sType == "BEG": |
| | p2 = self._makePerp2DVector(C, E, -1 * (shrt + abs(self.offsetExtra))) |
| | p3 = self._makePerp2DVector(p1, p2, shrt + abs(self.offsetExtra)) |
| | p4 = self._makePerp2DVector(p2, p3, (med + abs(self.offsetExtra))) |
| | p5 = self._makePerp2DVector(p3, p4, shrt + abs(self.offsetExtra)) |
| | elif sType == "END": |
| | p2 = self._makePerp2DVector(C, E, (shrt + abs(self.offsetExtra))) |
| | p3 = self._makePerp2DVector(p1, p2, -1 * (shrt + abs(self.offsetExtra))) |
| | p4 = self._makePerp2DVector( |
| | p2, p3, -1 * (med + abs(self.offsetExtra)) |
| | ) |
| | p5 = self._makePerp2DVector( |
| | p3, p4, -1 * (shrt + abs(self.offsetExtra)) |
| | ) |
| | p6 = p1 |
| | 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]) |
| | |
| | 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 |
| |
|
| | |
| | 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 |
| |
|