From c5e577be89c261365a0405e44bc2cde91d7c87c0 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 5 May 2020 13:20:08 -0500 Subject: [PATCH 01/11] Path: Initiate unification of ProfileFaces and ProfileEdges operations ProfileFaces now accepts and processes faces and edges. Full functionality is maintained (so far as tested) with respect to original operations. --- src/Mod/Path/PathScripts/PathAreaOp.py | 19 +- src/Mod/Path/PathScripts/PathProfileFaces.py | 1041 ++++++++++++++++-- src/Mod/Path/PathScripts/PathSelection.py | 7 +- 3 files changed, 990 insertions(+), 77 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathAreaOp.py b/src/Mod/Path/PathScripts/PathAreaOp.py index 8bcf1b4577..0d85ba646d 100644 --- a/src/Mod/Path/PathScripts/PathAreaOp.py +++ b/src/Mod/Path/PathScripts/PathAreaOp.py @@ -354,7 +354,7 @@ class ObjectOp(PathOp.ObjectOp): self.tempObjectNames = [] # pylint: disable=attribute-defined-outside-init self.stockBB = PathUtils.findParentJob(obj).Stock.Shape.BoundBox # pylint: disable=attribute-defined-outside-init self.useTempJobClones('Delete') # Clear temporary group and recreate for temp job clones - self.profileEdgesIsOpen = False + self.rotStartDepth = None # pylint: disable=attribute-defined-outside-init if obj.EnableRotation != 'Off': # Calculate operation heights based upon rotation radii @@ -371,6 +371,7 @@ class ObjectOp(PathOp.ObjectOp): strDep = max(self.xRotRad, self.yRotRad) finDep = -1 * strDep + self.rotStartDepth = strDep obj.ClearanceHeight.Value = strDep + self.clrOfset obj.SafeHeight.Value = strDep + self.safOfst @@ -419,15 +420,17 @@ class ObjectOp(PathOp.ObjectOp): shapes = [j['shape'] for j in jobs] - if self.profileEdgesIsOpen is True: - if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint: - osp = obj.StartPoint - self.commandlist.append(Path.Command('G0', {'X': osp.x, 'Y': osp.y, 'F': self.horizRapid})) - sims = [] numShapes = len(shapes) for ns in range(0, numShapes): + profileEdgesIsOpen = False (shape, isHole, sub, angle, axis, strDep, finDep) = shapes[ns] # pylint: disable=unused-variable + if sub == 'OpenEdge': + profileEdgesIsOpen = True + if PathOp.FeatureStartPoint & self.opFeatures(obj) and obj.UseStartPoint: + osp = obj.StartPoint + self.commandlist.append(Path.Command('G0', {'X': osp.x, 'Y': osp.y, 'F': self.horizRapid})) + if ns < numShapes - 1: nextAxis = shapes[ns + 1][4] else: @@ -436,7 +439,7 @@ class ObjectOp(PathOp.ObjectOp): self.depthparams = self._customDepthParams(obj, strDep, finDep) try: - if self.profileEdgesIsOpen is True: + if profileEdgesIsOpen: (pp, sim) = self._buildProfileOpenEdges(obj, shape, isHole, start, getsim) else: (pp, sim) = self._buildPathArea(obj, shape, isHole, start, getsim) @@ -444,7 +447,7 @@ class ObjectOp(PathOp.ObjectOp): FreeCAD.Console.PrintError(e) FreeCAD.Console.PrintError("Something unexpected happened. Check project and tool config.") else: - if self.profileEdgesIsOpen is True: + if profileEdgesIsOpen: ppCmds = pp else: ppCmds = pp.Commands diff --git a/src/Mod/Path/PathScripts/PathProfileFaces.py b/src/Mod/Path/PathScripts/PathProfileFaces.py index 281d848699..ee2189e628 100644 --- a/src/Mod/Path/PathScripts/PathProfileFaces.py +++ b/src/Mod/Path/PathScripts/PathProfileFaces.py @@ -30,6 +30,7 @@ import PathScripts.PathOp as PathOp import PathScripts.PathProfileBase as PathProfileBase import PathScripts.PathUtils as PathUtils import numpy +import math from PySide import QtCore @@ -37,6 +38,8 @@ from PySide import QtCore from lazy_loader.lazy_loader import LazyLoader ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') Part = LazyLoader('Part', globals(), 'Part') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + __title__ = "Path Profile Faces Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" @@ -63,7 +66,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): '''baseObject() ... returns super of receiver Used to call base implementation in overwritten functions.''' # return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureRotation - return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels + return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureBaseEdges def initAreaOp(self, obj): '''initAreaOp(obj) ... adds properties for hole, circle and perimeter processing.''' @@ -112,19 +115,114 @@ class ObjectProfile(PathProfileBase.ObjectProfile): '''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.''' PathLog.track() + shapes = [] + inaccessible = translate('PathProfileEdges', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') + baseSubsTuples = list() + allTuples = list() + edgeFaces = list() + subCount = 0 + self.profileshape = list() # pylint: disable=attribute-defined-outside-init + self.offsetExtra = abs(obj.OffsetExtra.Value) + + if PathLog.getLevel(PathLog.thisModule()) == 4: + 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)")) - shapes = [] - self.profileshape = [] # pylint: disable=attribute-defined-outside-init + # Pre-process Base Geometry, extracting edges + # Convert edges to wires, then to faces if possible + if obj.Base: # The user has selected subobjects from the base. Process each. + basewires = list() + delPairs = list() + ezMin = None + for p in range(0, len(obj.Base)): + (base, subsList) = obj.Base[p] + tmpSubs = list() + edgelist = list() + 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 + if isinstance(shape, Part.Face): + tmpSubs.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 faces + if len(tmpSubs) == 0: # all edges in subsList = remove pair in obj.Base + delPairs.append(p) + elif len(edgelist) > 0: # some edges in subsList were extracted, return faces only to subsList + obj.Base[p] = (base, tmpSubs) - baseSubsTuples = [] - subCount = 0 - allTuples = [] + for base, wires in basewires: + for wire in wires: + if wire.isClosed(): + # 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 = origWire.Wires[0] + if f: + # shift the compound to the bottom of the base object for proper sectioning + zShift = ezMin - f.BoundBox.ZMin + newPlace = FreeCAD.Placement(FreeCAD.Vector(0, 0, zShift), f.Placement.Rotation) + f.Placement = newPlace + env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) + # shapes.append((env, False)) + tup = env, False, 'ProfileEdges', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) + else: + PathLog.error(inaccessible) + else: + # Attempt open-edges profile + if self.JOB.GeometryTolerance.Value == 0.0: + msg = self.JOB.Label + '.GeometryTolerance = 0.0.' + msg += translate('PathProfileEdges', 'Please set to an acceptable value greater than zero.') + PathLog.error(msg) + else: + cutWireObjs = False + flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) + if flattened: + (origWire, flatWire) = flattened + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') + os.Shape = flatWire + os.purgeTouched() + self.tmpGrp.addObject(os) + cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) + if cutShp: + cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) + + if cutWireObjs: + for cW in cutWireObjs: + # shapes.append((cW, False)) + # self.profileEdgesIsOpen = True + tup = cW, False, 'OpenEdge', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) + else: + PathLog.error(inaccessible) + else: + PathLog.error(inaccessible) + # Efor + delPairs.sort(reverse=True) + for p in delPairs: + # obj.Base.pop(p) + pass if obj.Base: # The user has selected subobjects from the base. Process each. + isFace = False + isEdge = False if obj.EnableRotation != 'Off': for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] @@ -132,60 +230,13 @@ class ObjectProfile(PathProfileBase.ObjectProfile): subCount += 1 shape = getattr(base.Shape, sub) if isinstance(shape, Part.Face): - rtn = False - (norm, surf) = self.getFaceNormAndSurf(shape) - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) - if rtn is True: - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) - # Verify faces are correctly oriented - InverseAngle might be necessary - faceIA = getattr(clnBase.Shape, sub) - (norm, surf) = self.getFaceNormAndSurf(faceIA) - (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) - - if abs(praAngle) == 180.0: - rtn = False - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 1 is False') - angle -= 180.0 - - if rtn is True: - PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) - if obj.InverseAngle is False: - if obj.AttemptInverseAngle is True: - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - PathLog.warning(msg) - - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 2 is False') - angle += 180.0 - else: - PathLog.debug(' isFaceUp') - - else: - PathLog.debug("Face appears to be oriented correctly.") - - if angle < 0.0: - angle += 360.0 - - tup = clnBase, sub, tag, angle, axis, clnStock - else: - if self.warnDisabledAxis(obj, axis) is False: - PathLog.debug(str(sub) + ": No rotation used") - axis = 'X' - angle = 0.0 - tag = base.Name + '_' + axis + str(angle).replace('.', '_') - stock = PathUtils.findParentJob(obj).Stock - tup = base, sub, tag, angle, axis, stock - + tup = self._analyzeFace(obj, base, sub, shape, subCount) allTuples.append(tup) - + # Eif + # Efor if subCount > 1: - msg = translate('Path', "Multiple faces in Base Geometry.") + " " - msg += translate('Path', "Depth settings will be applied to all faces.") + msg = translate('PathProfile', "Multiple faces in Base Geometry.") + " " + msg += translate('PathProfile', "Depth settings will be applied to all faces.") PathLog.warning(msg) (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) @@ -198,6 +249,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): pair = base, subList, angle, axis, stock baseSubsTuples.append(pair) # Efor + else: PathLog.debug(translate("Path", "EnableRotation property is 'Off'.")) stock = PathUtils.findParentJob(obj).Stock @@ -224,15 +276,14 @@ class ObjectProfile(PathProfileBase.ObjectProfile): faceDepths.append(shape.BoundBox.ZMin) else: ignoreSub = base.Name + '.' + sub - msg = translate('Path', "Found a selected object which is not a face. Ignoring: {}".format(ignoreSub)) - PathLog.error(msg) - FreeCAD.Console.PrintWarning(msg) + msg = translate('PathProfile', "Found a selected object which is not a face. Ignoring:") + # FreeCAD.Console.PrintWarning(msg + " {}\n".format(ignoreSub)) # Set initial Start and Final Depths and recalculate depthparams finDep = obj.FinalDepth.Value strDep = obj.StartDepth.Value - if strDep > stock.Shape.BoundBox.ZMax: - strDep = stock.Shape.BoundBox.ZMax + # if strDep > stock.Shape.BoundBox.ZMax: + # strDep = stock.Shape.BoundBox.ZMax startDepths.append(strDep) self.depthparams = self._customDepthParams(obj, strDep, finDep) @@ -260,9 +311,9 @@ class ObjectProfile(PathProfileBase.ObjectProfile): try: # env = PathUtils.getEnvelope(base.Shape, subshape=profileshape, depthparams=envDepthparams) env = PathUtils.getEnvelope(profileshape, depthparams=envDepthparams) - except Exception: # pylint: disable=broad-except + except Exception as ee: # pylint: disable=broad-except # PathUtils.getEnvelope() failed to return an object. - PathLog.error(translate('Path', 'Unable to create path for face(s).')) + PathLog.error(translate('Path', 'Unable to create path for face(s).') + '\n{}'.format(ee)) else: tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep shapes.append(tup) @@ -284,9 +335,9 @@ class ObjectProfile(PathProfileBase.ObjectProfile): shapes.append(tup) # Lower high Start Depth to top of Stock - startDepth = max(startDepths) - if obj.StartDepth.Value > startDepth: - obj.StartDepth.Value = startDepth + # startDepth = max(startDepths) + # if obj.StartDepth.Value > startDepth: + # obj.StartDepth.Value = startDepth else: # Try to build targets from the job base if 1 == len(self.model): @@ -314,6 +365,13 @@ class ObjectProfile(PathProfileBase.ObjectProfile): self.removalshapes = shapes # pylint: disable=attribute-defined-outside-init PathLog.debug("%d shapes" % len(shapes)) + # Delete the temporary objects + if PathLog.getLevel(PathLog.thisModule()) == 4: + if FreeCAD.GuiUp: + import FreeCADGui + FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False + self.tmpGrp.purgeTouched() + return shapes def areaOpSetDefaultValues(self, obj, job): @@ -329,6 +387,853 @@ class ObjectProfile(PathProfileBase.ObjectProfile): obj.LimitDepthToFace = True obj.HandleMultipleFeatures = 'Individually' + # Analyze a face for rotational needs + def _analyzeFace(self, obj, base, sub, shape, subCount): + rtn = False + (norm, surf) = self.getFaceNormAndSurf(shape) + (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) + if rtn is True: + (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) + # Verify faces are correctly oriented - InverseAngle might be necessary + faceIA = getattr(clnBase.Shape, sub) + (norm, surf) = self.getFaceNormAndSurf(faceIA) + (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) + + if abs(praAngle) == 180.0: + rtn = False + if self.isFaceUp(clnBase, faceIA) is False: + PathLog.debug('isFaceUp 1 is False') + angle -= 180.0 + + if rtn is True: + PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) + if obj.InverseAngle is False: + if obj.AttemptInverseAngle is True: + (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) + else: + msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") + PathLog.warning(msg) + + if self.isFaceUp(clnBase, faceIA) is False: + PathLog.debug('isFaceUp 2 is False') + angle += 180.0 + else: + PathLog.debug(' isFaceUp') + + else: + PathLog.debug("Face appears to be oriented correctly.") + + if angle < 0.0: + angle += 360.0 + + tup = clnBase, sub, tag, angle, axis, clnStock + else: + if self.warnDisabledAxis(obj, axis) is False: + PathLog.debug(str(sub) + ": No rotation used") + axis = 'X' + angle = 0.0 + tag = base.Name + '_' + axis + str(angle).replace('.', '_') + stock = PathUtils.findParentJob(obj).Stock + tup = base, sub, tag, angle, axis, stock + + return tup + + # Edges pre-processing + def _flattenWire(self, obj, wire, trgtDep): + '''_flattenWire(obj, wire)... Return a flattened version of the wire''' + PathLog.debug('_flattenWire()') + wBB = wire.BoundBox + + if wBB.ZLength > 0.0: + PathLog.debug('Wire is not horizontally co-planar. Flattening it.') + + # Extrude non-horizontal wire + extFwdLen = wBB.ZLength * 2.2 + 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): + PathLog.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 = translate('PathProfile', + 'For open edges, select top edge and set Final Depth manually.') + FreeCAD.Console.PrintError(msg + '\n') + return False + 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) + + # Identify endpoints connecting circle center and diameter + vectDist = pe.sub(pb) + diam = vectDist.Length + cntr = vectDist.multiply(0.5).add(pb) + R = diam / 2 + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + # 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) + if PathLog.getLevel(PathLog.thisModule()) == 4: + CA = FCAD.addObject('Part::Feature', 'tmpCutArea') + CA.Shape = cutArea + CA.recompute() + CA.purgeTouched() + self.tmpGrp.addObject(CA) + + + # Get top and bottom faces of cut area (CA), and combine faces when necessary + topFc = list() + botFc = list() + 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: + PathLog.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: + PathLog.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: + PathLog.debug('Cutting on Ext side.') + self.cutSide = 'E' + self.cutSideTags = eTAG + tagCOM = begExt.CenterOfMass + else: + PathLog.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 + fcShp = workShp + wire = origWire + WS = workShp.Wires + lenWS = len(WS) + if lenWS < 3: + wi = 0 + 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: + PathLog.error('The cut area cross-section wire does not coincide with selected edge. Wires[] index is None.') + return False + else: + PathLog.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: + PathLog.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) + PathLog.debug('isReady {}.'.format(isReady)) + + if isReady is False: + PathLog.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: + PathLog.debug('offset area is less than minArea of {}.'.format(minArea)) + PathLog.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) + return cutShp + + def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): + # 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: + PathLog.debug('Cutting on Ext side.') + if cutSide == 'E': + return True + else: + PathLog.debug('Cutting on Int side.') + if cutSide == 'I': + return True + return False + + def _extractPathWire(self, obj, base, flatWire, cutShp): + PathLog.debug('_extractPathWire()') + + subLoops = list() + rtnWIRES = list() + osWrIdxs = list() + subDistFactor = 1.0 # Raise to include sub wires at greater distance from original + fdv = obj.FinalDepth.Value + wire = flatWire + lstVrtIdx = len(wire.Vertexes) - 1 + lstVrt = wire.Vertexes[lstVrtIdx] + frstVrt = wire.Vertexes[0] + cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv) + cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv) + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + # Calculate offset shape, containing cut region + ofstShp = self._extractFaceOffset(obj, cutShp, False) + + # CHECK for ZERO area of offset shape + try: + osArea = ofstShp.Area + except Exception as ee: + PathLog.error('No area to offset shape returned.') + return False + + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') + os.Shape = ofstShp + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) + + numOSWires = len(ofstShp.Wires) + for w in range(0, numOSWires): + osWrIdxs.append(w) + + # Identify two vertexes for dividing offset loop + NEAR0 = self._findNearestVertex(ofstShp, cent0) + min0i = 0 + min0 = NEAR0[0][4] + for n in range(0, len(NEAR0)): + N = NEAR0[n] + if N[4] < min0: + min0 = N[4] + min0i = n + (w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i + if PathLog.getLevel(PathLog.thisModule()) == 4: + near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0') + near0.Shape = Part.makeLine(cent0, pnt0) + near0.recompute() + near0.purgeTouched() + self.tmpGrp.addObject(near0) + + NEAR1 = self._findNearestVertex(ofstShp, cent1) + min1i = 0 + min1 = NEAR1[0][4] + for n in range(0, len(NEAR1)): + N = NEAR1[n] + if N[4] < min1: + min1 = N[4] + min1i = n + (w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i + if PathLog.getLevel(PathLog.thisModule()) == 4: + near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1') + near1.Shape = Part.makeLine(cent1, pnt1) + near1.recompute() + near1.purgeTouched() + self.tmpGrp.addObject(near1) + + if w0 != w1: + PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1)) + + if PathLog.getLevel(PathLog.thisModule()) == 4: + PathLog.debug('min0i is {}.'.format(min0i)) + PathLog.debug('min1i is {}.'.format(min1i)) + PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) + PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) + PathLog.debug('NEAR0 is {}.'.format(NEAR0)) + PathLog.debug('NEAR1 is {}.'.format(NEAR1)) + + mainWire = ofstShp.Wires[w0] + + # Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements + if numOSWires > 1: + # check all wires for proximity(children) to intersection tags + tagsComList = list() + for T in self.cutSideTags.Faces: + tcom = T.CenterOfMass + tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0) + tagsComList.append(tv) + subDist = self.ofstRadius * subDistFactor + for w in osWrIdxs: + if w != w0: + cutSub = False + VTXS = ofstShp.Wires[w].Vertexes + for V in VTXS: + v = FreeCAD.Vector(V.X, V.Y, 0.0) + for t in tagsComList: + if t.sub(v).Length < subDist: + cutSub = True + break + if cutSub is True: + break + if cutSub is True: + sub = Part.Wire(Part.__sortEdges__(ofstShp.Wires[w].Edges)) + subLoops.append(sub) + # Eif + + # Break offset loop into two wires - one of which is the desired profile path wire. + (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]) + edgs0 = list() + edgs1 = list() + for e in edgeIdxs0: + edgs0.append(mainWire.Edges[e]) + for e in edgeIdxs1: + edgs1.append(mainWire.Edges[e]) + part0 = Part.Wire(Part.__sortEdges__(edgs0)) + part1 = Part.Wire(Part.__sortEdges__(edgs1)) + + # Determine which part is nearest original edge(s) + distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0]) + distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0]) + if distToPart0 < distToPart1: + rtnWIRES.append(part0) + else: + rtnWIRES.append(part1) + rtnWIRES.extend(subLoops) + + return rtnWIRES + + def _extractFaceOffset(self, obj, fcShape, isHole): + '''_extractFaceOffset(obj, fcShape, isHole) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic - https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_extractFaceOffset()') + + areaParams = {} + JOB = PathUtils.findParentJob(obj) + tolrnc = JOB.GeometryTolerance.Value + if self.useComp is True: + offset = self.ofstRadius # + tolrnc + else: + offset = self.offsetExtra # + tolrnc + + if isHole is False: + offset = 0 - offset + + areaParams['Offset'] = offset + areaParams['Fill'] = 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + # areaParams['JoinType'] = 1 + + area = Path.Area() # Create instance of Area() class object + area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.add(fcShape) # obj.Shape to use for extracting offset + area.setParams(**areaParams) # set parameters + + return area.getShape() + + def _findNearestVertex(self, shape, point): + PathLog.debug('_findNearestVertex()') + PT = FreeCAD.Vector(point.x, point.y, 0.0) + + def sortDist(tup): + return tup[4] + + PNTS = list() + 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): + PathLog.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) + + lenE = len(wire.Edges) + FLGS = list() + for e in range(0, lenE): + FLGS.append(0) + + chk4 = False + for e in range(0, lenE): + 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 + PathLog.debug('_separateWireAtVertexes() FLGS: \n{}'.format(FLGS)) + + PRE = list() + POST = list() + IDXS = list() + IDX1 = list() + IDX2 = list() + for e in range(0, lenE): + 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 + begFlg = False + 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 + endIdx = None + for e in range(begIdx + 1, lenE + 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 begFlg is False: + begFlg = True + else: + begIdx = e + break + # find first 3 edge and group all first wire edges + endIdx = None + for e in range(begIdx, lenE + 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 + + if PathLog.getLevel(PathLog.thisModule()) != 4: + PathLog.debug('grps[0]: {}'.format(grps[0])) + PathLog.debug('grps[1]: {}'.format(grps[1])) + PathLog.debug('wireIdxs[0]: {}'.format(wireIdxs[0])) + PathLog.debug('wireIdxs[1]: {}'.format(wireIdxs[1])) + PathLog.debug('PRE: {}'.format(PRE)) + PathLog.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.''' + # Create cross-section of shape and translate + wires = list() + 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): + 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): + # Create circular probe tags around perimiter of wire + extTags = list() + intTags = list() + 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, 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 + cp1 = E.valueAt(E.getParameterByLength(d - spc)) + cp2 = E.valueAt(E.getParameterByLength(d + spc)) + (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): + 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)) + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + # 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): + rad = self.radius + ofstRad = self.ofstRadius + extra = self.radius / 10 + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + E = FreeCAD.Vector(pB.x, pB.y, 0) # endpoint + C = FreeCAD.Vector(pA.x, pA.y, 0) # checkpoint + lenEC = E.sub(C).Length + + if self.useComp is True or (self.useComp is False and self.offsetExtra != 0): + # 'L' stop shape and edge legend + # --1-- + # | | + # 2 6 + # | | + # | ----5----| + # | 4 + # -----3-------| + # positive dist in _makePerp2DVector() is CCW rotation + p1 = E + if sType == 'BEG': + p2 = self._makePerp2DVector(C, E, -0.25) # E1 + p3 = self._makePerp2DVector(p1, p2, ofstRad + 1 + extra) # E2 + p4 = self._makePerp2DVector(p2, p3, 0.25 + ofstRad + extra) # E3 + p5 = self._makePerp2DVector(p3, p4, 1 + extra) # E4 + p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) # E5 + elif sType == 'END': + p2 = self._makePerp2DVector(C, E, 0.25) # E1 + p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + 1 + extra)) # E2 + p4 = self._makePerp2DVector(p2, p3, -1 * (0.25 + ofstRad + extra)) # E3 + p5 = self._makePerp2DVector(p3, p4, -1 * (1 + 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 legend + # : + # |----2-------| + # 3 1 + # |-----4------| + # positive dist in _makePerp2DVector() is CCW rotation + p1 = E + if sType == 'BEG': + p2 = self._makePerp2DVector(C, E, -1 * (0.25 + abs(self.offsetExtra))) # left, 0.25 + p3 = self._makePerp2DVector(p1, p2, 0.25 + abs(self.offsetExtra)) + p4 = self._makePerp2DVector(p2, p3, (0.5 + abs(self.offsetExtra))) # FIRST POINT + p5 = self._makePerp2DVector(p3, p4, 0.25 + abs(self.offsetExtra)) # E1 SECOND + elif sType == 'END': + p2 = self._makePerp2DVector(C, E, (0.25 + abs(self.offsetExtra))) # left, 0.25 + p3 = self._makePerp2DVector(p1, p2, -1 * (0.25 + abs(self.offsetExtra))) + p4 = self._makePerp2DVector(p2, p3, -1 * (0.5 + abs(self.offsetExtra))) # FIRST POINT + p5 = self._makePerp2DVector(p3, p4, -1 * (0.25 + 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) + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl) + os.Shape = face + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) + + 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 range(0, len(wire.Edges)): + E = wire.Edges[e] + 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 SetupProperties(): setup = PathProfileBase.SetupProperties() diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py index 0cccde13b3..e34f03e699 100644 --- a/src/Mod/Path/PathScripts/PathSelection.py +++ b/src/Mod/Path/PathScripts/PathSelection.py @@ -209,7 +209,11 @@ def chamferselect(): def profileselect(): - FreeCADGui.Selection.addSelectionGate(PROFILEGate()) + gate = False + if(PROFILEGate() or EGate()): + gate = True + FreeCADGui.Selection.addSelectionGate(gate) + # FreeCADGui.Selection.addSelectionGate(PROFILEGate()) FreeCAD.Console.PrintWarning("Profiling Select Mode\n") @@ -249,6 +253,7 @@ def select(op): opsel['Pocket Shape'] = pocketselect opsel['Profile Edges'] = eselect opsel['Profile Faces'] = profileselect + opsel['Profile'] = profileselect opsel['Surface'] = surfaceselect opsel['Waterline'] = surfaceselect opsel['Adaptive'] = adaptiveselect From 78735ddc06170e93276e8ab300109ea922e38d6a Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Fri, 1 May 2020 09:17:06 -0500 Subject: [PATCH 02/11] Path: Add reference to parent class within child class --- src/Mod/Path/PathScripts/PathOpGui.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index 9a8a100810..42145249ee 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -204,6 +204,12 @@ class TaskPanelPage(object): self.setIcon(None) self.features = features self.isdirty = False + self.parent = None + + def setParent(self, parent): + '''setParent() ... used to transfer parent object link to child class. + Do not overwrite.''' + self.parent = parent def onDirtyChanged(self, callback): '''onDirtyChanged(callback) ... set callback when dirty state changes.''' @@ -882,6 +888,7 @@ class TaskPanel(object): for page in self.featurePages: page.initPage(obj) page.onDirtyChanged(self.pageDirtyChanged) + page.setParent(self) taskPanelLayout = PathPreferences.defaultTaskPanelLayout() From 16cf71bb7d5b433a551925ae00bc19a320fe929f Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Fri, 1 May 2020 10:51:08 -0500 Subject: [PATCH 03/11] Path: Add method, 'updatePanelVisibility()' New method allows one panel to update visibility on another panel. --- src/Mod/Path/PathScripts/PathOpGui.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index 42145249ee..1675a1856e 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -205,6 +205,7 @@ class TaskPanelPage(object): self.features = features self.isdirty = False self.parent = None + self.panelTitle = 'Operation' def setParent(self, parent): '''setParent() ... used to transfer parent object link to child class. @@ -393,11 +394,27 @@ class TaskPanelPage(object): if obj.CoolantMode != option: obj.CoolantMode = option + def updatePanelVisibility(self, panelTitle, obj): + if hasattr(self, 'parent'): + parent = getattr(self, 'parent') + if parent and hasattr(parent, 'featurePages'): + for page in parent.featurePages: + if hasattr(page, 'panelTitle'): + if page.panelTitle == panelTitle and hasattr(page, 'updateVisibility'): + page.updateVisibility(obj) + break + + class TaskPanelBaseGeometryPage(TaskPanelPage): '''Page controller for the base geometry.''' DataObject = QtCore.Qt.ItemDataRole.UserRole DataObjectSub = QtCore.Qt.ItemDataRole.UserRole + 1 + def __init__(self, obj, features): + super(TaskPanelBaseGeometryPage, self).__init__(obj, features) + + self.panelTitle = 'Base Geometry' + def getForm(self): return FreeCADGui.PySideUic.loadUi(":/panels/PageBaseGeometryEdit.ui") @@ -476,7 +493,6 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): return False return True - def addBaseGeometry(self, selection): PathLog.track(selection) if self.selectionSupportedAsBaseGeometry(selection, False): @@ -547,6 +563,7 @@ class TaskPanelBaseLocationPage(TaskPanelPage): # members initialized later self.editRow = None + self.panelTitle = 'Base Location' def getForm(self): self.formLoc = FreeCADGui.PySideUic.loadUi(":/panels/PageBaseLocationEdit.ui") @@ -662,6 +679,7 @@ class TaskPanelHeightsPage(TaskPanelPage): # members initialized later self.clearanceHeight = None self.safeHeight = None + self.panelTitle = 'Heights' def getForm(self): return FreeCADGui.PySideUic.loadUi(":/panels/PageHeightsEdit.ui") @@ -703,6 +721,7 @@ class TaskPanelDepthsPage(TaskPanelPage): self.finalDepth = None self.finishDepth = None self.stepDown = None + self.panelTitle = 'Depths' def getForm(self): return FreeCADGui.PySideUic.loadUi(":/panels/PageDepthsEdit.ui") From 52fe25528aae64a1af348a3d29d35ec94c86a29c Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Fri, 1 May 2020 09:19:56 -0500 Subject: [PATCH 04/11] Path: Update 'Operation' panel visibility when 'Base Geometry' changes --- src/Mod/Path/PathScripts/PathOpGui.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index 1675a1856e..2ef8264d81 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -506,6 +506,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): if self.addBaseGeometry(FreeCADGui.Selection.getSelectionEx()): # self.obj.Proxy.execute(self.obj) self.setFields(self.obj) + self.updatePanelVisibility('Operation', self.obj) self.setDirty() def deleteBase(self): @@ -513,6 +514,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): selected = self.form.baseList.selectedItems() for item in selected: self.form.baseList.takeItem(self.form.baseList.row(item)) + self.updatePanelVisibility('Operation', self.obj) self.setDirty() self.updateBase() # self.obj.Proxy.execute(self.obj) @@ -535,6 +537,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): def clearBase(self): self.obj.Base = [] + self.updatePanelVisibility('Operation', self.obj) self.setDirty() def registerSignalHandlers(self, obj): @@ -553,6 +556,7 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): else: self.form.addBase.setEnabled(False) + class TaskPanelBaseLocationPage(TaskPanelPage): '''Page controller for base locations. Uses PathGetPoint.''' From 27c4db9a345f20358fbb45ae9ec96f556a3e9c4f Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 5 May 2020 13:09:47 -0500 Subject: [PATCH 05/11] Path: Consolidate Contour, ProfileFaces, and ProfileEdges No geometry selection defaults to Contour operation. Path: Add new unified `Profile` operation modules --- src/Mod/Path/PathScripts/PathProfile.py | 1393 +++++++++++++++++ src/Mod/Path/PathScripts/PathProfileFaces.py | 1041 +----------- .../Path/PathScripts/PathProfileFacesGui.py | 106 +- src/Mod/Path/PathScripts/PathProfileGui.py | 173 ++ 4 files changed, 1687 insertions(+), 1026 deletions(-) create mode 100644 src/Mod/Path/PathScripts/PathProfile.py create mode 100644 src/Mod/Path/PathScripts/PathProfileGui.py diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py new file mode 100644 index 0000000000..6652b63a50 --- /dev/null +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -0,0 +1,1393 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2014 Yorik van Havre * +# * 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 PathScripts.PathLog as PathLog +import PathScripts.PathOp as PathOp +import PathScripts.PathAreaOp as PathAreaOp +import PathScripts.PathUtils as PathUtils +import numpy +import math + +from PySide import QtCore + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') +Part = LazyLoader('Part', globals(), 'Part') +DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') + + +__title__ = "Path Profile Faces Operation" +__author__ = "sliptonic (Brad Collette), Schildkroet" +__url__ = "http://www.freecadweb.org" +__doc__ = "Path Profile operation based on faces." + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class ObjectProfile(PathAreaOp.ObjectOp): + '''Proxy object for Profile operations based on faces.''' + + def baseObject(self): + '''baseObject() ... returns super of receiver + Used to call base implementation in overwritten functions.''' + return super(self.__class__, self) + + def areaOpFeatures(self, obj): + '''areaOpFeatures(obj) ... returns features specific to the operation''' + return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureBaseEdges + + def initAreaOp(self, obj): + '''initAreaOp(obj) ... creates all profile specific properties.''' + self.initAreaOpProperties(obj) + + obj.setEditorMode('MiterLimit', 2) + obj.setEditorMode('JoinType', 2) + + def initAreaOpProperties(self, obj, warn=False): + '''initAreaOpProperties(obj) ... create operation specific properties''' + missing = list() + JOB = PathUtils.findParentJob(obj) + + for (prtyp, nm, grp, tt) in self.areaOpProperties(): + if not hasattr(obj, nm): + obj.addProperty(prtyp, nm, grp, tt) + missing.append(nm) + if warn: + newPropMsg = translate('PathProfile', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' + newPropMsg += translate('PathProfile', 'Check its default value.') + PathLog.warning(newPropMsg) + + if len(missing) > 0: + # Set enumeration lists for enumeration properties + ENUMS = self.areaOpPropertyEnumerations() + for n in ENUMS: + if n in missing: + setattr(obj, n, ENUMS[n]) + # Set default values + PROP_DFLTS = self.areaOpPropertyDefaults(obj, JOB) + for n in PROP_DFLTS: + if n in missing: + setattr(obj, n, PROP_DFLTS[n]) + + 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", + QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)")), + ("App::PropertyEnumeration", "HandleMultipleFeatures", "Profile", + QtCore.QT_TRANSLATE_NOOP("PathPocket", "Choose how to process multiple Base Geometry features.")), + ("App::PropertyEnumeration", "JoinType", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool moves around corners. Default=Round")), + ("App::PropertyFloat", "MiterLimit", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Maximum distance before a miter join is truncated")), + ("App::PropertyDistance", "OffsetExtra", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Extra value to stay away from final profile- good for roughing toolpath")), + ("App::PropertyBool", "processHoles", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile holes as well as the outline")), + ("App::PropertyBool", "processPerimeter", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the outline")), + ("App::PropertyBool", "processCircles", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile round holes")), + ("App::PropertyEnumeration", "Side", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut")), + ("App::PropertyBool", "UseComp", "Profile", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if using Cutter Radius Compensation")), + + ("App::PropertyBool", "ReverseDirection", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Reverse direction of pocket operation.")), + ("App::PropertyBool", "InverseAngle", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Inverse the angle. Example: -22.5 -> 22.5 degrees.")), + ("App::PropertyBool", "AttemptInverseAngle", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Attempt the inverse angle for face access if original rotation fails.")), + ("App::PropertyBool", "LimitDepthToFace", "Rotation", + QtCore.QT_TRANSLATE_NOOP("App::Property", "Enforce the Z-depth of the selected face as the lowest value for final depth. Higher user values will be observed.")) + ] + + def areaOpPropertyEnumerations(self): + '''areaOpPropertyEnumerations() ... returns a dictionary of enumeration lists + for the operation's enumeration type properties.''' + # Enumeration lists for App::PropertyEnumeration properties + return { + 'Direction': ['CW', 'CCW'], # this is the direction that the profile runs + 'HandleMultipleFeatures': ['Collectively', 'Individually'], + 'JoinType': ['Round', 'Square', 'Miter'], # this is the direction that the Profile runs + 'Side': ['Outside', 'Inside'], # side of profile that cutter is on in relation to direction of profile + } + + def areaOpPropertyDefaults(self, obj=None, job=None): + '''areaOpPropertyDefaults(obj=None, job=None) ... returns a dictionary of default values + for the operation's properties.''' + return { + 'AttemptInverseAngle': True, + 'Direction': 'CW', + 'HandleMultipleFeatures': 'Individually', + 'InverseAngle': False, + 'JoinType': 'Round', + 'LimitDepthToFace': True, + 'MiterLimit': 0.1, + 'OffsetExtra': 0.0, + 'ReverseDirection': False, + 'Side': 'Outside', + 'UseComp': True, + 'processCircles': False, + 'processHoles': False, + 'processPerimeter': True + } + + def areaOpOnChanged(self, obj, prop): + '''areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.''' + if prop in ['UseComp', 'JoinType', 'EnableRotation']: + self.setOpEditorProperties(obj) + + def setOpEditorProperties(self, obj): + '''setOpEditorProperties(obj, porp) ... Process operation-specific changes to properties visibility.''' + side = 2 + if obj.UseComp: + if len(obj.Base) > 0: + side = 0 + obj.setEditorMode('Side', side) + + if obj.JoinType == 'Miter': + obj.setEditorMode('MiterLimit', 0) + else: + obj.setEditorMode('MiterLimit', 2) + + rotation = 2 + if obj.EnableRotation != 'Off': + rotation = 0 + obj.setEditorMode('ReverseDirection', rotation) + obj.setEditorMode('InverseAngle', rotation) + obj.setEditorMode('AttemptInverseAngle', rotation) + obj.setEditorMode('LimitDepthToFace', rotation) + + def areaOpOnDocumentRestored(self, obj): + self.initAreaOpProperties(obj, warn=True) + + for prop in ['UseComp', 'JoinType']: + self.areaOpOnChanged(obj, prop) + + 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 = 0.0 + if obj.UseComp: + offset = self.radius + obj.OffsetExtra.Value + if obj.Side == 'Inside': + offset = 0 - offset + if isHole: + offset = 0 - offset + params['Offset'] = offset + + jointype = ['Round', 'Square', 'Miter'] + params['JoinType'] = jointype.index(obj.JoinType) + + if obj.JoinType == 'Miter': + params['MiterLimit'] = obj.MiterLimit + + 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 + + if not obj.UseComp: + if direction == 'CCW': + params['orientation'] = 1 + else: + params['orientation'] = 0 + + return params + + def areaOpUseProjection(self, obj): + '''areaOpUseProjection(obj) ... returns True''' + return True + + def opUpdateDepths(self, obj): + obj.OpStartDepth = obj.OpStockZMax + obj.OpFinalDepth = obj.OpStockZMin + + def areaOpShapes(self, obj): + '''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.''' + PathLog.track() + + shapes = [] + inaccessible = translate('PathProfileEdges', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') + baseSubsTuples = list() + allTuples = list() + edgeFaces = list() + subCount = 0 + self.profileshape = list() # pylint: disable=attribute-defined-outside-init + self.offsetExtra = abs(obj.OffsetExtra.Value) + + if PathLog.getLevel(PathLog.thisModule()) == 4: + 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)) + + if obj.Base and len(obj.Base) > 0: # The user has selected subobjects from the base. Process each. + isFace = False + isEdge = False + if obj.EnableRotation != 'Off': + for p in range(0, len(obj.Base)): + (base, subsList) = obj.Base[p] + for sub in subsList: + subCount += 1 + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + tup = self._analyzeFace(obj, base, sub, shape, subCount) + allTuples.append(tup) + # Eif + # Efor + if subCount > 1: + msg = translate('PathProfile', "Multiple faces in Base Geometry.") + " " + msg += translate('PathProfile', "Depth settings will be applied to all faces.") + PathLog.warning(msg) + + (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) + subList = [] + for o in range(0, len(Tags)): + subList = [] + for (base, sub, tag, angle, axis, stock) in Grps[o]: + subList.append(sub) + + pair = base, subList, angle, axis, stock + baseSubsTuples.append(pair) + # Efor + else: + PathLog.debug(translate("Path", "EnableRotation property is 'Off'.")) + stock = PathUtils.findParentJob(obj).Stock + for (base, subList) in obj.Base: + baseSubsTuples.append((base, subList, 0.0, 'X', stock)) + # Eif + + # for base in obj.Base: + finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 + for (base, subsList, angle, axis, stock) in baseSubsTuples: + holes = [] + faces = [] + faceDepths = [] + startDepths = [] + + for sub in subsList: + shape = getattr(base.Shape, sub) + if isinstance(shape, Part.Face): + faces.append(shape) + if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face + for wire in shape.Wires[1:]: + holes.append((base.Shape, wire)) + + # Add face depth to list + faceDepths.append(shape.BoundBox.ZMin) + else: + ignoreSub = base.Name + '.' + sub + msg = translate('PathProfile', "Found a selected object which is not a face. Ignoring:") + # FreeCAD.Console.PrintWarning(msg + " {}\n".format(ignoreSub)) + + # Set initial Start and Final Depths and recalculate depthparams + finDep = obj.FinalDepth.Value + strDep = obj.StartDepth.Value + # if strDep > stock.Shape.BoundBox.ZMax: + # strDep = stock.Shape.BoundBox.ZMax + + startDepths.append(strDep) + self.depthparams = self._customDepthParams(obj, strDep, finDep) + + for shape, wire in holes: + f = Part.makeFace(wire, 'Part::FaceMakerSimple') + drillable = PathUtils.isDrillable(shape, wire) + if (drillable and obj.processCircles) or (not drillable and obj.processHoles): + env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams) + tup = env, True, 'pathProfileFaces', angle, axis, strDep, finDep + shapes.append(tup) + + if len(faces) > 0: + profileshape = Part.makeCompound(faces) + self.profileshape.append(profileshape) + + if obj.processPerimeter: + if obj.HandleMultipleFeatures == 'Collectively': + custDepthparams = self.depthparams + + if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off': + if profileshape.BoundBox.ZMin > obj.FinalDepth.Value: + finDep = profileshape.BoundBox.ZMin + envDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope + try: + # env = PathUtils.getEnvelope(base.Shape, subshape=profileshape, depthparams=envDepthparams) + env = PathUtils.getEnvelope(profileshape, depthparams=envDepthparams) + except Exception as ee: # pylint: disable=broad-except + # PathUtils.getEnvelope() failed to return an object. + PathLog.error(translate('Path', 'Unable to create path for face(s).') + '\n{}'.format(ee)) + else: + tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep + shapes.append(tup) + + elif obj.HandleMultipleFeatures == 'Individually': + for shape in faces: + # profShape = Part.makeCompound([shape]) + finalDep = obj.FinalDepth.Value + custDepthparams = self.depthparams + if obj.Side == 'Inside': + if finalDep < shape.BoundBox.ZMin: + # Recalculate depthparams + finalDep = shape.BoundBox.ZMin + custDepthparams = self._customDepthParams(obj, strDep + 0.5, finalDep) + + # env = PathUtils.getEnvelope(base.Shape, subshape=profShape, depthparams=custDepthparams) + env = PathUtils.getEnvelope(shape, depthparams=custDepthparams) + tup = env, False, 'pathProfileFaces', angle, axis, strDep, finalDep + shapes.append(tup) + + else: # Try to build targets from the job base + self.opUpdateDepths(obj) + + if 1 == len(self.model) and hasattr(self.model[0], "Proxy"): + if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet + modelProxy = self.model[0].Proxy + # Process circles and holes if requested by user + if obj.processCircles or obj.processHoles: + for shape in modelProxy.getHoles(self.model[0], transform=True): + for wire in shape.Wires: + drillable = PathUtils.isDrillable(modelProxy, wire) + if (drillable and obj.processCircles) or (not drillable and obj.processHoles): + f = Part.makeFace(wire, 'Part::FaceMakerSimple') + env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) + tup = env, True, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) + + # Process perimeter if requested by user + if obj.processPerimeter: + for shape in modelProxy.getOutlines(self.model[0], transform=True): + for wire in shape.Wires: + f = Part.makeFace(wire, 'Part::FaceMakerSimple') + env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) + tup = env, False, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) + else: + shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]) + else: + shapes.extend([(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')]) + + self.removalshapes = shapes # pylint: disable=attribute-defined-outside-init + PathLog.debug("%d shapes" % len(shapes)) + + # Delete the temporary objects + if PathLog.getLevel(PathLog.thisModule()) == 4: + if FreeCAD.GuiUp: + import FreeCADGui + FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False + self.tmpGrp.purgeTouched() + + return shapes + + # Analyze a face for rotational needs + def _analyzeFace(self, obj, base, sub, shape, subCount): + rtn = False + (norm, surf) = self.getFaceNormAndSurf(shape) + (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) + if rtn is True: + (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) + # Verify faces are correctly oriented - InverseAngle might be necessary + faceIA = getattr(clnBase.Shape, sub) + (norm, surf) = self.getFaceNormAndSurf(faceIA) + (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) + + if abs(praAngle) == 180.0: + rtn = False + if self.isFaceUp(clnBase, faceIA) is False: + PathLog.debug('isFaceUp 1 is False') + angle -= 180.0 + + if rtn is True: + PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) + if obj.InverseAngle is False: + if obj.AttemptInverseAngle is True: + (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) + else: + msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") + PathLog.warning(msg) + + if self.isFaceUp(clnBase, faceIA) is False: + PathLog.debug('isFaceUp 2 is False') + angle += 180.0 + else: + PathLog.debug(' isFaceUp') + + else: + PathLog.debug("Face appears to be oriented correctly.") + + if angle < 0.0: + angle += 360.0 + + tup = clnBase, sub, tag, angle, axis, clnStock + else: + if self.warnDisabledAxis(obj, axis) is False: + PathLog.debug(str(sub) + ": No rotation used") + axis = 'X' + angle = 0.0 + tag = base.Name + '_' + axis + str(angle).replace('.', '_') + stock = PathUtils.findParentJob(obj).Stock + tup = base, sub, tag, angle, axis, stock + + return tup + + # Edges pre-processing + def _processEdges(self, obj): + shapes = list() + basewires = list() + delPairs = list() + ezMin = None + for p in range(0, len(obj.Base)): + (base, subsList) = obj.Base[p] + tmpSubs = list() + edgelist = list() + 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 + if isinstance(shape, Part.Face): + tmpSubs.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 faces + if len(tmpSubs) == 0: # all edges in subsList = remove pair in obj.Base + delPairs.append(p) + elif len(edgelist) > 0: # some edges in subsList were extracted, return faces only to subsList + obj.Base[p] = (base, tmpSubs) + + for base, wires in basewires: + for wire in wires: + if wire.isClosed(): + # 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 = origWire.Wires[0] + if f: + # shift the compound to the bottom of the base object for proper sectioning + zShift = ezMin - f.BoundBox.ZMin + newPlace = FreeCAD.Placement(FreeCAD.Vector(0, 0, zShift), f.Placement.Rotation) + f.Placement = newPlace + env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) + # shapes.append((env, False)) + tup = env, False, 'ProfileEdges', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) + else: + PathLog.error(inaccessible) + else: + # Attempt open-edges profile + if self.JOB.GeometryTolerance.Value == 0.0: + msg = self.JOB.Label + '.GeometryTolerance = 0.0.' + msg += translate('PathProfileEdges', 'Please set to an acceptable value greater than zero.') + PathLog.error(msg) + else: + cutWireObjs = False + flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) + if flattened: + (origWire, flatWire) = flattened + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') + os.Shape = flatWire + os.purgeTouched() + self.tmpGrp.addObject(os) + cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) + if cutShp: + cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) + + if cutWireObjs: + for cW in cutWireObjs: + # shapes.append((cW, False)) + # self.profileEdgesIsOpen = True + tup = cW, False, 'OpenEdge', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value + shapes.append(tup) + else: + PathLog.error(inaccessible) + else: + PathLog.error(inaccessible) + # Eif + # Eif + # Efor + # Efor + + delPairs.sort(reverse=True) + for p in delPairs: + # obj.Base.pop(p) + pass + + return shapes + + def _flattenWire(self, obj, wire, trgtDep): + '''_flattenWire(obj, wire)... Return a flattened version of the wire''' + PathLog.debug('_flattenWire()') + wBB = wire.BoundBox + + if wBB.ZLength > 0.0: + PathLog.debug('Wire is not horizontally co-planar. Flattening it.') + + # Extrude non-horizontal wire + extFwdLen = wBB.ZLength * 2.2 + 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): + PathLog.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 = translate('PathProfile', + 'For open edges, select top edge and set Final Depth manually.') + FreeCAD.Console.PrintError(msg + '\n') + return False + 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) + + # Identify endpoints connecting circle center and diameter + vectDist = pe.sub(pb) + diam = vectDist.Length + cntr = vectDist.multiply(0.5).add(pb) + R = diam / 2 + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + # 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) + if PathLog.getLevel(PathLog.thisModule()) == 4: + CA = FCAD.addObject('Part::Feature', 'tmpCutArea') + CA.Shape = cutArea + CA.recompute() + CA.purgeTouched() + self.tmpGrp.addObject(CA) + + + # Get top and bottom faces of cut area (CA), and combine faces when necessary + topFc = list() + botFc = list() + 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: + PathLog.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: + PathLog.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: + PathLog.debug('Cutting on Ext side.') + self.cutSide = 'E' + self.cutSideTags = eTAG + tagCOM = begExt.CenterOfMass + else: + PathLog.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 + fcShp = workShp + wire = origWire + WS = workShp.Wires + lenWS = len(WS) + if lenWS < 3: + wi = 0 + 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: + PathLog.error('The cut area cross-section wire does not coincide with selected edge. Wires[] index is None.') + return False + else: + PathLog.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: + PathLog.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) + PathLog.debug('isReady {}.'.format(isReady)) + + if isReady is False: + PathLog.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: + PathLog.debug('offset area is less than minArea of {}.'.format(minArea)) + PathLog.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) + return cutShp + + def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): + # 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: + PathLog.debug('Cutting on Ext side.') + if cutSide == 'E': + return True + else: + PathLog.debug('Cutting on Int side.') + if cutSide == 'I': + return True + return False + + def _extractPathWire(self, obj, base, flatWire, cutShp): + PathLog.debug('_extractPathWire()') + + subLoops = list() + rtnWIRES = list() + osWrIdxs = list() + subDistFactor = 1.0 # Raise to include sub wires at greater distance from original + fdv = obj.FinalDepth.Value + wire = flatWire + lstVrtIdx = len(wire.Vertexes) - 1 + lstVrt = wire.Vertexes[lstVrtIdx] + frstVrt = wire.Vertexes[0] + cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv) + cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv) + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + # Calculate offset shape, containing cut region + ofstShp = self._extractFaceOffset(obj, cutShp, False) + + # CHECK for ZERO area of offset shape + try: + osArea = ofstShp.Area + except Exception as ee: + PathLog.error('No area to offset shape returned.') + return False + + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') + os.Shape = ofstShp + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) + + numOSWires = len(ofstShp.Wires) + for w in range(0, numOSWires): + osWrIdxs.append(w) + + # Identify two vertexes for dividing offset loop + NEAR0 = self._findNearestVertex(ofstShp, cent0) + min0i = 0 + min0 = NEAR0[0][4] + for n in range(0, len(NEAR0)): + N = NEAR0[n] + if N[4] < min0: + min0 = N[4] + min0i = n + (w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i + if PathLog.getLevel(PathLog.thisModule()) == 4: + near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0') + near0.Shape = Part.makeLine(cent0, pnt0) + near0.recompute() + near0.purgeTouched() + self.tmpGrp.addObject(near0) + + NEAR1 = self._findNearestVertex(ofstShp, cent1) + min1i = 0 + min1 = NEAR1[0][4] + for n in range(0, len(NEAR1)): + N = NEAR1[n] + if N[4] < min1: + min1 = N[4] + min1i = n + (w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i + if PathLog.getLevel(PathLog.thisModule()) == 4: + near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1') + near1.Shape = Part.makeLine(cent1, pnt1) + near1.recompute() + near1.purgeTouched() + self.tmpGrp.addObject(near1) + + if w0 != w1: + PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1)) + + if PathLog.getLevel(PathLog.thisModule()) == 4: + PathLog.debug('min0i is {}.'.format(min0i)) + PathLog.debug('min1i is {}.'.format(min1i)) + PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) + PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) + PathLog.debug('NEAR0 is {}.'.format(NEAR0)) + PathLog.debug('NEAR1 is {}.'.format(NEAR1)) + + mainWire = ofstShp.Wires[w0] + + # Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements + if numOSWires > 1: + # check all wires for proximity(children) to intersection tags + tagsComList = list() + for T in self.cutSideTags.Faces: + tcom = T.CenterOfMass + tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0) + tagsComList.append(tv) + subDist = self.ofstRadius * subDistFactor + for w in osWrIdxs: + if w != w0: + cutSub = False + VTXS = ofstShp.Wires[w].Vertexes + for V in VTXS: + v = FreeCAD.Vector(V.X, V.Y, 0.0) + for t in tagsComList: + if t.sub(v).Length < subDist: + cutSub = True + break + if cutSub is True: + break + if cutSub is True: + sub = Part.Wire(Part.__sortEdges__(ofstShp.Wires[w].Edges)) + subLoops.append(sub) + # Eif + + # Break offset loop into two wires - one of which is the desired profile path wire. + (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]) + edgs0 = list() + edgs1 = list() + for e in edgeIdxs0: + edgs0.append(mainWire.Edges[e]) + for e in edgeIdxs1: + edgs1.append(mainWire.Edges[e]) + part0 = Part.Wire(Part.__sortEdges__(edgs0)) + part1 = Part.Wire(Part.__sortEdges__(edgs1)) + + # Determine which part is nearest original edge(s) + distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0]) + distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0]) + if distToPart0 < distToPart1: + rtnWIRES.append(part0) + else: + rtnWIRES.append(part1) + rtnWIRES.extend(subLoops) + + return rtnWIRES + + def _extractFaceOffset(self, obj, fcShape, isHole): + '''_extractFaceOffset(obj, fcShape, isHole) ... internal function. + Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. + Adjustments made based on notes by @sliptonic - https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' + PathLog.debug('_extractFaceOffset()') + + areaParams = {} + JOB = PathUtils.findParentJob(obj) + tolrnc = JOB.GeometryTolerance.Value + if self.useComp is True: + offset = self.ofstRadius # + tolrnc + else: + offset = self.offsetExtra # + tolrnc + + if isHole is False: + offset = 0 - offset + + areaParams['Offset'] = offset + areaParams['Fill'] = 1 + areaParams['Coplanar'] = 0 + areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections + areaParams['Reorient'] = True + areaParams['OpenMode'] = 0 + areaParams['MaxArcPoints'] = 400 # 400 + areaParams['Project'] = True + # areaParams['JoinType'] = 1 + + area = Path.Area() # Create instance of Area() class object + area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane + area.add(fcShape) # obj.Shape to use for extracting offset + area.setParams(**areaParams) # set parameters + + return area.getShape() + + def _findNearestVertex(self, shape, point): + PathLog.debug('_findNearestVertex()') + PT = FreeCAD.Vector(point.x, point.y, 0.0) + + def sortDist(tup): + return tup[4] + + PNTS = list() + 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): + PathLog.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) + + lenE = len(wire.Edges) + FLGS = list() + for e in range(0, lenE): + FLGS.append(0) + + chk4 = False + for e in range(0, lenE): + 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 + PathLog.debug('_separateWireAtVertexes() FLGS: \n{}'.format(FLGS)) + + PRE = list() + POST = list() + IDXS = list() + IDX1 = list() + IDX2 = list() + for e in range(0, lenE): + 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 + begFlg = False + 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 + endIdx = None + for e in range(begIdx + 1, lenE + 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 begFlg is False: + begFlg = True + else: + begIdx = e + break + # find first 3 edge and group all first wire edges + endIdx = None + for e in range(begIdx, lenE + 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 + + if PathLog.getLevel(PathLog.thisModule()) != 4: + PathLog.debug('grps[0]: {}'.format(grps[0])) + PathLog.debug('grps[1]: {}'.format(grps[1])) + PathLog.debug('wireIdxs[0]: {}'.format(wireIdxs[0])) + PathLog.debug('wireIdxs[1]: {}'.format(wireIdxs[1])) + PathLog.debug('PRE: {}'.format(PRE)) + PathLog.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.''' + # Create cross-section of shape and translate + wires = list() + 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): + 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): + # Create circular probe tags around perimiter of wire + extTags = list() + intTags = list() + 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, 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 + cp1 = E.valueAt(E.getParameterByLength(d - spc)) + cp2 = E.valueAt(E.getParameterByLength(d + spc)) + (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): + 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)) + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + # 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): + rad = self.radius + ofstRad = self.ofstRadius + extra = self.radius / 10 + + pl = FreeCAD.Placement() + pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) + pl.Base = FreeCAD.Vector(0, 0, 0) + + E = FreeCAD.Vector(pB.x, pB.y, 0) # endpoint + C = FreeCAD.Vector(pA.x, pA.y, 0) # checkpoint + lenEC = E.sub(C).Length + + if self.useComp is True or (self.useComp is False and self.offsetExtra != 0): + # 'L' stop shape and edge legend + # --1-- + # | | + # 2 6 + # | | + # | ----5----| + # | 4 + # -----3-------| + # positive dist in _makePerp2DVector() is CCW rotation + p1 = E + if sType == 'BEG': + p2 = self._makePerp2DVector(C, E, -0.25) # E1 + p3 = self._makePerp2DVector(p1, p2, ofstRad + 1 + extra) # E2 + p4 = self._makePerp2DVector(p2, p3, 0.25 + ofstRad + extra) # E3 + p5 = self._makePerp2DVector(p3, p4, 1 + extra) # E4 + p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) # E5 + elif sType == 'END': + p2 = self._makePerp2DVector(C, E, 0.25) # E1 + p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + 1 + extra)) # E2 + p4 = self._makePerp2DVector(p2, p3, -1 * (0.25 + ofstRad + extra)) # E3 + p5 = self._makePerp2DVector(p3, p4, -1 * (1 + 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 legend + # : + # |----2-------| + # 3 1 + # |-----4------| + # positive dist in _makePerp2DVector() is CCW rotation + p1 = E + if sType == 'BEG': + p2 = self._makePerp2DVector(C, E, -1 * (0.25 + abs(self.offsetExtra))) # left, 0.25 + p3 = self._makePerp2DVector(p1, p2, 0.25 + abs(self.offsetExtra)) + p4 = self._makePerp2DVector(p2, p3, (0.5 + abs(self.offsetExtra))) # FIRST POINT + p5 = self._makePerp2DVector(p3, p4, 0.25 + abs(self.offsetExtra)) # E1 SECOND + elif sType == 'END': + p2 = self._makePerp2DVector(C, E, (0.25 + abs(self.offsetExtra))) # left, 0.25 + p3 = self._makePerp2DVector(p1, p2, -1 * (0.25 + abs(self.offsetExtra))) + p4 = self._makePerp2DVector(p2, p3, -1 * (0.5 + abs(self.offsetExtra))) # FIRST POINT + p5 = self._makePerp2DVector(p3, p4, -1 * (0.25 + 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) + if PathLog.getLevel(PathLog.thisModule()) == 4: + os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl) + os.Shape = face + os.recompute() + os.purgeTouched() + self.tmpGrp.addObject(os) + + 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 range(0, len(wire.Edges)): + E = wire.Edges[e] + 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 SetupProperties(): + setup = PathAreaOp.SetupProperties() + setup.extend([tup[1] for tup in ObjectProfile.areaOpProperties(False)]) + return setup + + +def Create(name, obj=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) + return obj diff --git a/src/Mod/Path/PathScripts/PathProfileFaces.py b/src/Mod/Path/PathScripts/PathProfileFaces.py index ee2189e628..281d848699 100644 --- a/src/Mod/Path/PathScripts/PathProfileFaces.py +++ b/src/Mod/Path/PathScripts/PathProfileFaces.py @@ -30,7 +30,6 @@ import PathScripts.PathOp as PathOp import PathScripts.PathProfileBase as PathProfileBase import PathScripts.PathUtils as PathUtils import numpy -import math from PySide import QtCore @@ -38,8 +37,6 @@ from PySide import QtCore from lazy_loader.lazy_loader import LazyLoader ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') Part = LazyLoader('Part', globals(), 'Part') -DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') - __title__ = "Path Profile Faces Operation" __author__ = "sliptonic (Brad Collette), Schildkroet" @@ -66,7 +63,7 @@ class ObjectProfile(PathProfileBase.ObjectProfile): '''baseObject() ... returns super of receiver Used to call base implementation in overwritten functions.''' # return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureRotation - return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureBaseEdges + return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels def initAreaOp(self, obj): '''initAreaOp(obj) ... adds properties for hole, circle and perimeter processing.''' @@ -115,114 +112,19 @@ class ObjectProfile(PathProfileBase.ObjectProfile): '''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.''' PathLog.track() - shapes = [] - inaccessible = translate('PathProfileEdges', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') - baseSubsTuples = list() - allTuples = list() - edgeFaces = list() - subCount = 0 - self.profileshape = list() # pylint: disable=attribute-defined-outside-init - self.offsetExtra = abs(obj.OffsetExtra.Value) - - if PathLog.getLevel(PathLog.thisModule()) == 4: - 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, extracting edges - # Convert edges to wires, then to faces if possible - if obj.Base: # The user has selected subobjects from the base. Process each. - basewires = list() - delPairs = list() - ezMin = None - for p in range(0, len(obj.Base)): - (base, subsList) = obj.Base[p] - tmpSubs = list() - edgelist = list() - 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 - if isinstance(shape, Part.Face): - tmpSubs.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 faces - if len(tmpSubs) == 0: # all edges in subsList = remove pair in obj.Base - delPairs.append(p) - elif len(edgelist) > 0: # some edges in subsList were extracted, return faces only to subsList - obj.Base[p] = (base, tmpSubs) + shapes = [] + self.profileshape = [] # pylint: disable=attribute-defined-outside-init - for base, wires in basewires: - for wire in wires: - if wire.isClosed(): - # 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 = origWire.Wires[0] - if f: - # shift the compound to the bottom of the base object for proper sectioning - zShift = ezMin - f.BoundBox.ZMin - newPlace = FreeCAD.Placement(FreeCAD.Vector(0, 0, zShift), f.Placement.Rotation) - f.Placement = newPlace - env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) - # shapes.append((env, False)) - tup = env, False, 'ProfileEdges', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value - shapes.append(tup) - else: - PathLog.error(inaccessible) - else: - # Attempt open-edges profile - if self.JOB.GeometryTolerance.Value == 0.0: - msg = self.JOB.Label + '.GeometryTolerance = 0.0.' - msg += translate('PathProfileEdges', 'Please set to an acceptable value greater than zero.') - PathLog.error(msg) - else: - cutWireObjs = False - flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) - if flattened: - (origWire, flatWire) = flattened - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') - os.Shape = flatWire - os.purgeTouched() - self.tmpGrp.addObject(os) - cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) - if cutShp: - cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) - - if cutWireObjs: - for cW in cutWireObjs: - # shapes.append((cW, False)) - # self.profileEdgesIsOpen = True - tup = cW, False, 'OpenEdge', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value - shapes.append(tup) - else: - PathLog.error(inaccessible) - else: - PathLog.error(inaccessible) - # Efor - delPairs.sort(reverse=True) - for p in delPairs: - # obj.Base.pop(p) - pass + baseSubsTuples = [] + subCount = 0 + allTuples = [] if obj.Base: # The user has selected subobjects from the base. Process each. - isFace = False - isEdge = False if obj.EnableRotation != 'Off': for p in range(0, len(obj.Base)): (base, subsList) = obj.Base[p] @@ -230,13 +132,60 @@ class ObjectProfile(PathProfileBase.ObjectProfile): subCount += 1 shape = getattr(base.Shape, sub) if isinstance(shape, Part.Face): - tup = self._analyzeFace(obj, base, sub, shape, subCount) + rtn = False + (norm, surf) = self.getFaceNormAndSurf(shape) + (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) + if rtn is True: + (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) + # Verify faces are correctly oriented - InverseAngle might be necessary + faceIA = getattr(clnBase.Shape, sub) + (norm, surf) = self.getFaceNormAndSurf(faceIA) + (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable + PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) + + if abs(praAngle) == 180.0: + rtn = False + if self.isFaceUp(clnBase, faceIA) is False: + PathLog.debug('isFaceUp 1 is False') + angle -= 180.0 + + if rtn is True: + PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) + if obj.InverseAngle is False: + if obj.AttemptInverseAngle is True: + (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) + else: + msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") + PathLog.warning(msg) + + if self.isFaceUp(clnBase, faceIA) is False: + PathLog.debug('isFaceUp 2 is False') + angle += 180.0 + else: + PathLog.debug(' isFaceUp') + + else: + PathLog.debug("Face appears to be oriented correctly.") + + if angle < 0.0: + angle += 360.0 + + tup = clnBase, sub, tag, angle, axis, clnStock + else: + if self.warnDisabledAxis(obj, axis) is False: + PathLog.debug(str(sub) + ": No rotation used") + axis = 'X' + angle = 0.0 + tag = base.Name + '_' + axis + str(angle).replace('.', '_') + stock = PathUtils.findParentJob(obj).Stock + tup = base, sub, tag, angle, axis, stock + allTuples.append(tup) - # Eif - # Efor + if subCount > 1: - msg = translate('PathProfile', "Multiple faces in Base Geometry.") + " " - msg += translate('PathProfile', "Depth settings will be applied to all faces.") + msg = translate('Path', "Multiple faces in Base Geometry.") + " " + msg += translate('Path', "Depth settings will be applied to all faces.") PathLog.warning(msg) (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) @@ -249,7 +198,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): pair = base, subList, angle, axis, stock baseSubsTuples.append(pair) # Efor - else: PathLog.debug(translate("Path", "EnableRotation property is 'Off'.")) stock = PathUtils.findParentJob(obj).Stock @@ -276,14 +224,15 @@ class ObjectProfile(PathProfileBase.ObjectProfile): faceDepths.append(shape.BoundBox.ZMin) else: ignoreSub = base.Name + '.' + sub - msg = translate('PathProfile', "Found a selected object which is not a face. Ignoring:") - # FreeCAD.Console.PrintWarning(msg + " {}\n".format(ignoreSub)) + msg = translate('Path', "Found a selected object which is not a face. Ignoring: {}".format(ignoreSub)) + PathLog.error(msg) + FreeCAD.Console.PrintWarning(msg) # Set initial Start and Final Depths and recalculate depthparams finDep = obj.FinalDepth.Value strDep = obj.StartDepth.Value - # if strDep > stock.Shape.BoundBox.ZMax: - # strDep = stock.Shape.BoundBox.ZMax + if strDep > stock.Shape.BoundBox.ZMax: + strDep = stock.Shape.BoundBox.ZMax startDepths.append(strDep) self.depthparams = self._customDepthParams(obj, strDep, finDep) @@ -311,9 +260,9 @@ class ObjectProfile(PathProfileBase.ObjectProfile): try: # env = PathUtils.getEnvelope(base.Shape, subshape=profileshape, depthparams=envDepthparams) env = PathUtils.getEnvelope(profileshape, depthparams=envDepthparams) - except Exception as ee: # pylint: disable=broad-except + except Exception: # pylint: disable=broad-except # PathUtils.getEnvelope() failed to return an object. - PathLog.error(translate('Path', 'Unable to create path for face(s).') + '\n{}'.format(ee)) + PathLog.error(translate('Path', 'Unable to create path for face(s).')) else: tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep shapes.append(tup) @@ -335,9 +284,9 @@ class ObjectProfile(PathProfileBase.ObjectProfile): shapes.append(tup) # Lower high Start Depth to top of Stock - # startDepth = max(startDepths) - # if obj.StartDepth.Value > startDepth: - # obj.StartDepth.Value = startDepth + startDepth = max(startDepths) + if obj.StartDepth.Value > startDepth: + obj.StartDepth.Value = startDepth else: # Try to build targets from the job base if 1 == len(self.model): @@ -365,13 +314,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): self.removalshapes = shapes # pylint: disable=attribute-defined-outside-init PathLog.debug("%d shapes" % len(shapes)) - # Delete the temporary objects - if PathLog.getLevel(PathLog.thisModule()) == 4: - if FreeCAD.GuiUp: - import FreeCADGui - FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False - self.tmpGrp.purgeTouched() - return shapes def areaOpSetDefaultValues(self, obj, job): @@ -387,853 +329,6 @@ class ObjectProfile(PathProfileBase.ObjectProfile): obj.LimitDepthToFace = True obj.HandleMultipleFeatures = 'Individually' - # Analyze a face for rotational needs - def _analyzeFace(self, obj, base, sub, shape, subCount): - rtn = False - (norm, surf) = self.getFaceNormAndSurf(shape) - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) - if rtn is True: - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) - # Verify faces are correctly oriented - InverseAngle might be necessary - faceIA = getattr(clnBase.Shape, sub) - (norm, surf) = self.getFaceNormAndSurf(faceIA) - (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) - - if abs(praAngle) == 180.0: - rtn = False - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 1 is False') - angle -= 180.0 - - if rtn is True: - PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) - if obj.InverseAngle is False: - if obj.AttemptInverseAngle is True: - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - PathLog.warning(msg) - - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 2 is False') - angle += 180.0 - else: - PathLog.debug(' isFaceUp') - - else: - PathLog.debug("Face appears to be oriented correctly.") - - if angle < 0.0: - angle += 360.0 - - tup = clnBase, sub, tag, angle, axis, clnStock - else: - if self.warnDisabledAxis(obj, axis) is False: - PathLog.debug(str(sub) + ": No rotation used") - axis = 'X' - angle = 0.0 - tag = base.Name + '_' + axis + str(angle).replace('.', '_') - stock = PathUtils.findParentJob(obj).Stock - tup = base, sub, tag, angle, axis, stock - - return tup - - # Edges pre-processing - def _flattenWire(self, obj, wire, trgtDep): - '''_flattenWire(obj, wire)... Return a flattened version of the wire''' - PathLog.debug('_flattenWire()') - wBB = wire.BoundBox - - if wBB.ZLength > 0.0: - PathLog.debug('Wire is not horizontally co-planar. Flattening it.') - - # Extrude non-horizontal wire - extFwdLen = wBB.ZLength * 2.2 - 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): - PathLog.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 = translate('PathProfile', - 'For open edges, select top edge and set Final Depth manually.') - FreeCAD.Console.PrintError(msg + '\n') - return False - 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) - - # Identify endpoints connecting circle center and diameter - vectDist = pe.sub(pb) - diam = vectDist.Length - cntr = vectDist.multiply(0.5).add(pb) - R = diam / 2 - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - # 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) - if PathLog.getLevel(PathLog.thisModule()) == 4: - CA = FCAD.addObject('Part::Feature', 'tmpCutArea') - CA.Shape = cutArea - CA.recompute() - CA.purgeTouched() - self.tmpGrp.addObject(CA) - - - # Get top and bottom faces of cut area (CA), and combine faces when necessary - topFc = list() - botFc = list() - 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: - PathLog.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: - PathLog.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: - PathLog.debug('Cutting on Ext side.') - self.cutSide = 'E' - self.cutSideTags = eTAG - tagCOM = begExt.CenterOfMass - else: - PathLog.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 - fcShp = workShp - wire = origWire - WS = workShp.Wires - lenWS = len(WS) - if lenWS < 3: - wi = 0 - 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: - PathLog.error('The cut area cross-section wire does not coincide with selected edge. Wires[] index is None.') - return False - else: - PathLog.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: - PathLog.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) - PathLog.debug('isReady {}.'.format(isReady)) - - if isReady is False: - PathLog.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: - PathLog.debug('offset area is less than minArea of {}.'.format(minArea)) - PathLog.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) - return cutShp - - def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): - # 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: - PathLog.debug('Cutting on Ext side.') - if cutSide == 'E': - return True - else: - PathLog.debug('Cutting on Int side.') - if cutSide == 'I': - return True - return False - - def _extractPathWire(self, obj, base, flatWire, cutShp): - PathLog.debug('_extractPathWire()') - - subLoops = list() - rtnWIRES = list() - osWrIdxs = list() - subDistFactor = 1.0 # Raise to include sub wires at greater distance from original - fdv = obj.FinalDepth.Value - wire = flatWire - lstVrtIdx = len(wire.Vertexes) - 1 - lstVrt = wire.Vertexes[lstVrtIdx] - frstVrt = wire.Vertexes[0] - cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv) - cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv) - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - # Calculate offset shape, containing cut region - ofstShp = self._extractFaceOffset(obj, cutShp, False) - - # CHECK for ZERO area of offset shape - try: - osArea = ofstShp.Area - except Exception as ee: - PathLog.error('No area to offset shape returned.') - return False - - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') - os.Shape = ofstShp - os.recompute() - os.purgeTouched() - self.tmpGrp.addObject(os) - - numOSWires = len(ofstShp.Wires) - for w in range(0, numOSWires): - osWrIdxs.append(w) - - # Identify two vertexes for dividing offset loop - NEAR0 = self._findNearestVertex(ofstShp, cent0) - min0i = 0 - min0 = NEAR0[0][4] - for n in range(0, len(NEAR0)): - N = NEAR0[n] - if N[4] < min0: - min0 = N[4] - min0i = n - (w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i - if PathLog.getLevel(PathLog.thisModule()) == 4: - near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0') - near0.Shape = Part.makeLine(cent0, pnt0) - near0.recompute() - near0.purgeTouched() - self.tmpGrp.addObject(near0) - - NEAR1 = self._findNearestVertex(ofstShp, cent1) - min1i = 0 - min1 = NEAR1[0][4] - for n in range(0, len(NEAR1)): - N = NEAR1[n] - if N[4] < min1: - min1 = N[4] - min1i = n - (w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i - if PathLog.getLevel(PathLog.thisModule()) == 4: - near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1') - near1.Shape = Part.makeLine(cent1, pnt1) - near1.recompute() - near1.purgeTouched() - self.tmpGrp.addObject(near1) - - if w0 != w1: - PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1)) - - if PathLog.getLevel(PathLog.thisModule()) == 4: - PathLog.debug('min0i is {}.'.format(min0i)) - PathLog.debug('min1i is {}.'.format(min1i)) - PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) - PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) - PathLog.debug('NEAR0 is {}.'.format(NEAR0)) - PathLog.debug('NEAR1 is {}.'.format(NEAR1)) - - mainWire = ofstShp.Wires[w0] - - # Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements - if numOSWires > 1: - # check all wires for proximity(children) to intersection tags - tagsComList = list() - for T in self.cutSideTags.Faces: - tcom = T.CenterOfMass - tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0) - tagsComList.append(tv) - subDist = self.ofstRadius * subDistFactor - for w in osWrIdxs: - if w != w0: - cutSub = False - VTXS = ofstShp.Wires[w].Vertexes - for V in VTXS: - v = FreeCAD.Vector(V.X, V.Y, 0.0) - for t in tagsComList: - if t.sub(v).Length < subDist: - cutSub = True - break - if cutSub is True: - break - if cutSub is True: - sub = Part.Wire(Part.__sortEdges__(ofstShp.Wires[w].Edges)) - subLoops.append(sub) - # Eif - - # Break offset loop into two wires - one of which is the desired profile path wire. - (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]) - edgs0 = list() - edgs1 = list() - for e in edgeIdxs0: - edgs0.append(mainWire.Edges[e]) - for e in edgeIdxs1: - edgs1.append(mainWire.Edges[e]) - part0 = Part.Wire(Part.__sortEdges__(edgs0)) - part1 = Part.Wire(Part.__sortEdges__(edgs1)) - - # Determine which part is nearest original edge(s) - distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0]) - distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0]) - if distToPart0 < distToPart1: - rtnWIRES.append(part0) - else: - rtnWIRES.append(part1) - rtnWIRES.extend(subLoops) - - return rtnWIRES - - def _extractFaceOffset(self, obj, fcShape, isHole): - '''_extractFaceOffset(obj, fcShape, isHole) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic - https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - areaParams = {} - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - if self.useComp is True: - offset = self.ofstRadius # + tolrnc - else: - offset = self.offsetExtra # + tolrnc - - if isHole is False: - offset = 0 - offset - - areaParams['Offset'] = offset - areaParams['Fill'] = 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - # areaParams['JoinType'] = 1 - - area = Path.Area() # Create instance of Area() class object - area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.add(fcShape) # obj.Shape to use for extracting offset - area.setParams(**areaParams) # set parameters - - return area.getShape() - - def _findNearestVertex(self, shape, point): - PathLog.debug('_findNearestVertex()') - PT = FreeCAD.Vector(point.x, point.y, 0.0) - - def sortDist(tup): - return tup[4] - - PNTS = list() - 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): - PathLog.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) - - lenE = len(wire.Edges) - FLGS = list() - for e in range(0, lenE): - FLGS.append(0) - - chk4 = False - for e in range(0, lenE): - 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 - PathLog.debug('_separateWireAtVertexes() FLGS: \n{}'.format(FLGS)) - - PRE = list() - POST = list() - IDXS = list() - IDX1 = list() - IDX2 = list() - for e in range(0, lenE): - 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 - begFlg = False - 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 - endIdx = None - for e in range(begIdx + 1, lenE + 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 begFlg is False: - begFlg = True - else: - begIdx = e - break - # find first 3 edge and group all first wire edges - endIdx = None - for e in range(begIdx, lenE + 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 - - if PathLog.getLevel(PathLog.thisModule()) != 4: - PathLog.debug('grps[0]: {}'.format(grps[0])) - PathLog.debug('grps[1]: {}'.format(grps[1])) - PathLog.debug('wireIdxs[0]: {}'.format(wireIdxs[0])) - PathLog.debug('wireIdxs[1]: {}'.format(wireIdxs[1])) - PathLog.debug('PRE: {}'.format(PRE)) - PathLog.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.''' - # Create cross-section of shape and translate - wires = list() - 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): - 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): - # Create circular probe tags around perimiter of wire - extTags = list() - intTags = list() - 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, 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 - cp1 = E.valueAt(E.getParameterByLength(d - spc)) - cp2 = E.valueAt(E.getParameterByLength(d + spc)) - (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): - 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)) - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - # 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): - rad = self.radius - ofstRad = self.ofstRadius - extra = self.radius / 10 - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - E = FreeCAD.Vector(pB.x, pB.y, 0) # endpoint - C = FreeCAD.Vector(pA.x, pA.y, 0) # checkpoint - lenEC = E.sub(C).Length - - if self.useComp is True or (self.useComp is False and self.offsetExtra != 0): - # 'L' stop shape and edge legend - # --1-- - # | | - # 2 6 - # | | - # | ----5----| - # | 4 - # -----3-------| - # positive dist in _makePerp2DVector() is CCW rotation - p1 = E - if sType == 'BEG': - p2 = self._makePerp2DVector(C, E, -0.25) # E1 - p3 = self._makePerp2DVector(p1, p2, ofstRad + 1 + extra) # E2 - p4 = self._makePerp2DVector(p2, p3, 0.25 + ofstRad + extra) # E3 - p5 = self._makePerp2DVector(p3, p4, 1 + extra) # E4 - p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) # E5 - elif sType == 'END': - p2 = self._makePerp2DVector(C, E, 0.25) # E1 - p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + 1 + extra)) # E2 - p4 = self._makePerp2DVector(p2, p3, -1 * (0.25 + ofstRad + extra)) # E3 - p5 = self._makePerp2DVector(p3, p4, -1 * (1 + 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 legend - # : - # |----2-------| - # 3 1 - # |-----4------| - # positive dist in _makePerp2DVector() is CCW rotation - p1 = E - if sType == 'BEG': - p2 = self._makePerp2DVector(C, E, -1 * (0.25 + abs(self.offsetExtra))) # left, 0.25 - p3 = self._makePerp2DVector(p1, p2, 0.25 + abs(self.offsetExtra)) - p4 = self._makePerp2DVector(p2, p3, (0.5 + abs(self.offsetExtra))) # FIRST POINT - p5 = self._makePerp2DVector(p3, p4, 0.25 + abs(self.offsetExtra)) # E1 SECOND - elif sType == 'END': - p2 = self._makePerp2DVector(C, E, (0.25 + abs(self.offsetExtra))) # left, 0.25 - p3 = self._makePerp2DVector(p1, p2, -1 * (0.25 + abs(self.offsetExtra))) - p4 = self._makePerp2DVector(p2, p3, -1 * (0.5 + abs(self.offsetExtra))) # FIRST POINT - p5 = self._makePerp2DVector(p3, p4, -1 * (0.25 + 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) - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl) - os.Shape = face - os.recompute() - os.purgeTouched() - self.tmpGrp.addObject(os) - - 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 range(0, len(wire.Edges)): - E = wire.Edges[e] - 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 SetupProperties(): setup = PathProfileBase.SetupProperties() diff --git a/src/Mod/Path/PathScripts/PathProfileFacesGui.py b/src/Mod/Path/PathScripts/PathProfileFacesGui.py index 9deb81f00c..e56c35e0c8 100644 --- a/src/Mod/Path/PathScripts/PathProfileFacesGui.py +++ b/src/Mod/Path/PathScripts/PathProfileFacesGui.py @@ -1,53 +1,53 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * * -# * 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 PathScripts.PathOpGui as PathOpGui -import PathScripts.PathProfileBaseGui as PathProfileBaseGui -import PathScripts.PathProfileFaces as PathProfileFaces - -from PySide import QtCore - -__title__ = "Path Profile based on faces Operation UI" -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Profile based on faces operation page controller and command implementation." - -class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage): - '''Page controller for profile based on faces operation.''' - - def profileFeatures(self): - '''profileFeatures() ... return FeatureSide | FeatureProcessing. - See PathProfileBaseGui.py for details.''' - return PathProfileBaseGui.FeatureSide | PathProfileBaseGui.FeatureProcessing - -Command = PathOpGui.SetupOperation('Profile Faces', - PathProfileFaces.Create, - TaskPanelOpPage, - 'Path-Profile-Face', - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Face Profile"), - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on face or faces"), - PathProfileFaces.SetupProperties) - -FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * 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 PathScripts.PathOpGui as PathOpGui +import PathScripts.PathProfileBaseGui as PathProfileBaseGui +import PathScripts.PathProfileFaces as PathProfileFaces + +from PySide import QtCore + +__title__ = "Path Profile based on faces Operation UI" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Profile based on faces operation page controller and command implementation." + +class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage): + '''Page controller for profile based on faces operation.''' + + def profileFeatures(self): + '''profileFeatures() ... return FeatureSide | FeatureProcessing. + See PathProfileBaseGui.py for details.''' + return PathProfileBaseGui.FeatureSide | PathProfileBaseGui.FeatureProcessing + +Command = PathOpGui.SetupOperation('Profile Faces', + PathProfileFaces.Create, + TaskPanelOpPage, + 'Path-Profile-Face', + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Face Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on face or faces"), + PathProfileFaces.SetupProperties) + +FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py new file mode 100644 index 0000000000..5ddd8e872b --- /dev/null +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -0,0 +1,173 @@ +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * 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 FreeCADGui +import PathScripts.PathGui as PathGui +import PathScripts.PathOpGui as PathOpGui +import PathScripts.PathProfileFaces as PathProfileFaces + +from PySide import QtCore + + +__title__ = "Path Profile Operation UI" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Profile operation page controller and command implementation." + + +FeatureSide = 0x01 +FeatureProcessing = 0x02 + +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class TaskPanelOpPage(PathOpGui.TaskPanelPage): + '''Base class for profile operation page controllers. Two sub features are supported: + FeatureSide ... Is the Side property exposed in the UI + FeatureProcessing ... Are the processing check boxes supported by the operation + ''' + + def initPage(self, obj): + self.updateVisibility(obj) + + def profileFeatures(self): + '''profileFeatures() ... return which of the optional profile features are supported. + Currently two features are supported and returned: + FeatureSide ... Is the Side property exposed in the UI + FeatureProcessing ... Are the processing check boxes supported by the operation + .''' + return FeatureSide | FeatureProcessing + + def getForm(self): + '''getForm() ... returns UI customized according to profileFeatures()''' + form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui") + return form + + def getFields(self, obj): + '''getFields(obj) ... transfers values from UI to obj's proprties''' + self.updateToolController(obj, self.form.toolController) + self.updateCoolant(obj, self.form.coolantController) + + if obj.Side != str(self.form.cutSide.currentText()): + obj.Side = str(self.form.cutSide.currentText()) + if obj.Direction != str(self.form.direction.currentText()): + obj.Direction = str(self.form.direction.currentText()) + PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) + if obj.EnableRotation != str(self.form.enableRotation.currentText()): + obj.EnableRotation = str(self.form.enableRotation.currentText()) + + if obj.UseComp != self.form.useCompensation.isChecked(): + obj.UseComp = self.form.useCompensation.isChecked() + if obj.UseStartPoint != self.form.useStartPoint.isChecked(): + obj.UseStartPoint = self.form.useStartPoint.isChecked() + + if obj.processHoles != self.form.processHoles.isChecked(): + obj.processHoles = self.form.processHoles.isChecked() + if obj.processPerimeter != self.form.processPerimeter.isChecked(): + obj.processPerimeter = self.form.processPerimeter.isChecked() + if obj.processCircles != self.form.processCircles.isChecked(): + obj.processCircles = self.form.processCircles.isChecked() + + def setFields(self, obj): + '''setFields(obj) ... transfers obj's property values to UI''' + self.setupToolController(obj, self.form.toolController) + self.setupCoolant(obj, self.form.coolantController) + + self.selectInComboBox(obj.Side, self.form.cutSide) + self.selectInComboBox(obj.Direction, self.form.direction) + self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) + self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) + + self.form.useCompensation.setChecked(obj.UseComp) + self.form.useStartPoint.setChecked(obj.UseStartPoint) + self.form.processHoles.setChecked(obj.processHoles) + self.form.processPerimeter.setChecked(obj.processPerimeter) + self.form.processCircles.setChecked(obj.processCircles) + + self.updateVisibility(obj) + + def getSignalsForUpdate(self, obj): + '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' + signals = [] + signals.append(self.form.toolController.currentIndexChanged) + signals.append(self.form.coolantController.currentIndexChanged) + signals.append(self.form.cutSide.currentIndexChanged) + signals.append(self.form.direction.currentIndexChanged) + signals.append(self.form.extraOffset.editingFinished) + signals.append(self.form.enableRotation.currentIndexChanged) + signals.append(self.form.useCompensation.stateChanged) + signals.append(self.form.useStartPoint.stateChanged) + signals.append(self.form.processHoles.stateChanged) + signals.append(self.form.processPerimeter.stateChanged) + signals.append(self.form.processCircles.stateChanged) + + return signals + + def updateVisibility(self, obj): + hasFace = False + fullModel = False + if len(obj.Base) > 0: + for (base, subsList) in obj.Base: + for sub in subsList: + if sub[:4] == 'Face': + hasFace = True + break + else: + fullModel = True + + if hasFace: + self.form.processCircles.show() + self.form.processHoles.show() + self.form.processPerimeter.show() + else: + self.form.processCircles.hide() + self.form.processHoles.hide() + self.form.processPerimeter.hide() + + if self.form.useCompensation.isChecked() is True and not fullModel: + self.form.cutSide.show() + self.form.cutSideLabel.show() + else: + # Reset cutSide to 'Outside' for full model before hiding cutSide input + if self.form.cutSide.currentText() == 'Inside': + self.selectInComboBox('Outside', self.form.cutSide) + self.form.cutSide.hide() + self.form.cutSideLabel.hide() + + def registerSignalHandlers(self, obj): + self.form.useCompensation.stateChanged.connect(self.updateVisibility) +# Eclass + + +Command = PathOpGui.SetupOperation('Profile Faces', + PathProfileFaces.Create, + TaskPanelOpPage, + 'Path-Profile-Face', + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Face Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on face or faces"), + PathProfileFaces.SetupProperties) + +FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") From 900059bc99620db052bac941d656146a71893f0b Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Tue, 5 May 2020 13:45:48 -0500 Subject: [PATCH 06/11] Path: Integrate unified `Profile` operation into PathWB Remove Contour, Profile Faces, and Profile Edges icons from PathWB GUI. Files are still fully in tact and available. --- src/Mod/Path/CMakeLists.txt | 2 ++ src/Mod/Path/InitGui.py | 3 ++- src/Mod/Path/PathScripts/PathGuiInit.py | 7 ++++--- src/Mod/Path/PathScripts/PathProfileGui.py | 14 +++++++------- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 78415e1e32..73958e64d0 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -87,6 +87,7 @@ SET(PathScripts_SRCS PathScripts/PathPreferencesPathJob.py PathScripts/PathProbe.py PathScripts/PathProbeGui.py + PathScripts/PathProfile.py PathScripts/PathProfileBase.py PathScripts/PathProfileBaseGui.py PathScripts/PathProfileContour.py @@ -95,6 +96,7 @@ SET(PathScripts_SRCS PathScripts/PathProfileEdgesGui.py PathScripts/PathProfileFaces.py PathScripts/PathProfileFacesGui.py + PathScripts/PathProfileGui.py PathScripts/PathSanity.py PathScripts/PathSelection.py PathScripts/PathSetupSheet.py diff --git a/src/Mod/Path/InitGui.py b/src/Mod/Path/InitGui.py index 6fabab05f0..4546a78136 100644 --- a/src/Mod/Path/InitGui.py +++ b/src/Mod/Path/InitGui.py @@ -90,7 +90,8 @@ class PathWorkbench (Workbench): projcmdlist = ["Path_Job", "Path_Post"] toolcmdlist = ["Path_Inspect", "Path_Simulator", "Path_ToolLibraryEdit", "Path_SelectLoop", "Path_OpActiveToggle"] prepcmdlist = ["Path_Fixture", "Path_Comment", "Path_Stop", "Path_Custom", "Path_Probe"] - twodopcmdlist = ["Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"] + # twodopcmdlist = ["Path_Profile", "Path_Contour", "Path_Profile_Faces", "Path_Profile_Edges", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"] + twodopcmdlist = ["Path_Profile", "Path_Pocket_Shape", "Path_Drilling", "Path_MillFace", "Path_Helix", "Path_Adaptive"] threedopcmdlist = ["Path_Pocket_3D"] engravecmdlist = ["Path_Engrave", "Path_Deburr"] modcmdlist = ["Path_OperationCopy", "Path_Array", "Path_SimpleCopy"] diff --git a/src/Mod/Path/PathScripts/PathGuiInit.py b/src/Mod/Path/PathScripts/PathGuiInit.py index 52116e608b..21aadba1d5 100644 --- a/src/Mod/Path/PathScripts/PathGuiInit.py +++ b/src/Mod/Path/PathScripts/PathGuiInit.py @@ -64,9 +64,10 @@ def Startup(): from PathScripts import PathPocketShapeGui from PathScripts import PathPost from PathScripts import PathProbeGui - from PathScripts import PathProfileContourGui - from PathScripts import PathProfileEdgesGui - from PathScripts import PathProfileFacesGui + # from PathScripts import PathProfileContourGui + # from PathScripts import PathProfileEdgesGui + # from PathScripts import PathProfileFacesGui + from PathScripts import PathProfileGui from PathScripts import PathSanity from PathScripts import PathSetupSheetGui from PathScripts import PathSimpleCopy diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index 5ddd8e872b..756ff2bd30 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -26,7 +26,7 @@ import FreeCAD import FreeCADGui import PathScripts.PathGui as PathGui import PathScripts.PathOpGui as PathOpGui -import PathScripts.PathProfileFaces as PathProfileFaces +import PathScripts.PathProfile as PathProfile from PySide import QtCore @@ -162,12 +162,12 @@ class TaskPanelOpPage(PathOpGui.TaskPanelPage): # Eclass -Command = PathOpGui.SetupOperation('Profile Faces', - PathProfileFaces.Create, +Command = PathOpGui.SetupOperation('Profile', + PathProfile.Create, TaskPanelOpPage, - 'Path-Profile-Face', - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Face Profile"), - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on face or faces"), - PathProfileFaces.SetupProperties) + 'Path-Contour', + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), + PathProfile.SetupProperties) FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") From cf23bc689294826430e98826bc6fff1c5f5dceb5 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 7 May 2020 23:06:49 -0500 Subject: [PATCH 07/11] Path: Implement backwards compatibility Source modules are replaced with pass-through code to send pre-existing profile-based operations to new unified `Profile` operation. Path: Set line endings to Unix style --- src/Mod/Path/CMakeLists.txt | 2 - src/Mod/Path/PathScripts/PathProfileBase.py | 170 ---- .../Path/PathScripts/PathProfileBaseGui.py | 137 --- .../Path/PathScripts/PathProfileContour.py | 96 +- .../Path/PathScripts/PathProfileContourGui.py | 32 +- src/Mod/Path/PathScripts/PathProfileEdges.py | 929 +----------------- .../Path/PathScripts/PathProfileEdgesGui.py | 35 +- src/Mod/Path/PathScripts/PathProfileFaces.py | 322 +----- .../Path/PathScripts/PathProfileFacesGui.py | 107 +- 9 files changed, 128 insertions(+), 1702 deletions(-) delete mode 100644 src/Mod/Path/PathScripts/PathProfileBase.py delete mode 100644 src/Mod/Path/PathScripts/PathProfileBaseGui.py diff --git a/src/Mod/Path/CMakeLists.txt b/src/Mod/Path/CMakeLists.txt index 73958e64d0..12538fbe55 100644 --- a/src/Mod/Path/CMakeLists.txt +++ b/src/Mod/Path/CMakeLists.txt @@ -88,8 +88,6 @@ SET(PathScripts_SRCS PathScripts/PathProbe.py PathScripts/PathProbeGui.py PathScripts/PathProfile.py - PathScripts/PathProfileBase.py - PathScripts/PathProfileBaseGui.py PathScripts/PathProfileContour.py PathScripts/PathProfileContourGui.py PathScripts/PathProfileEdges.py diff --git a/src/Mod/Path/PathScripts/PathProfileBase.py b/src/Mod/Path/PathScripts/PathProfileBase.py deleted file mode 100644 index 2a685a5f92..0000000000 --- a/src/Mod/Path/PathScripts/PathProfileBase.py +++ /dev/null @@ -1,170 +0,0 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * Copyright (c) 2020 Schildkroet * -# * Copyright (c) 2020 russ4262 (Russell Johnson) * -# * * -# * 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 PathScripts.PathAreaOp as PathAreaOp -import PathScripts.PathLog as PathLog - -from PySide import QtCore - -__title__ = "Base Path Profile Operation" -__author__ = "sliptonic (Brad Collette), Schildkroet" -__url__ = "http://www.freecadweb.org" -__doc__ = "Base class and implementation for Path profile operations." - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) - - -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -class ObjectProfile(PathAreaOp.ObjectOp): - '''Base class for proxy objects of all profile operations.''' - - def initAreaOp(self, obj): - '''initAreaOp(obj) ... creates all profile specific properties. - Do not overwrite.''' - # Profile Properties - obj.addProperty("App::PropertyEnumeration", "Side", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Side of edge that tool should cut")) - obj.Side = ['Outside', 'Inside'] # side of profile that cutter is on in relation to direction of profile - obj.addProperty("App::PropertyEnumeration", "Direction", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "The direction that the toolpath should go around the part ClockWise (CW) or CounterClockWise (CCW)")) - obj.Direction = ['CW', 'CCW'] # this is the direction that the profile runs - obj.addProperty("App::PropertyBool", "UseComp", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Make True, if using Cutter Radius Compensation")) - - obj.addProperty("App::PropertyDistance", "OffsetExtra", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Extra value to stay away from final profile- good for roughing toolpath")) - obj.addProperty("App::PropertyEnumeration", "JoinType", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Controls how tool moves around corners. Default=Round")) - obj.JoinType = ['Round', 'Square', 'Miter'] # this is the direction that the Profile runs - obj.addProperty("App::PropertyFloat", "MiterLimit", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Maximum distance before a miter join is truncated")) - obj.setEditorMode('MiterLimit', 2) - - def areaOpOnChanged(self, obj, prop): - '''areaOpOnChanged(obj, prop) ... updates Side and MiterLimit visibility depending on changed properties. - Do not overwrite.''' - if prop == "UseComp": - if not obj.UseComp: - obj.setEditorMode('Side', 2) - else: - obj.setEditorMode('Side', 0) - - if prop == 'JoinType': - if obj.JoinType == 'Miter': - obj.setEditorMode('MiterLimit', 0) - else: - obj.setEditorMode('MiterLimit', 2) - - self.extraOpOnChanged(obj, prop) - - def extraOpOnChanged(self, obj, prop): - '''otherOpOnChanged(obj, porp) ... overwrite to process onChange() events. - Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def setOpEditorProperties(self, obj): - '''setOpEditorProperties(obj, porp) ... overwrite to process operation specific changes to properties. - Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def areaOpOnDocumentRestored(self, obj): - for prop in ['UseComp', 'JoinType']: - self.areaOpOnChanged(obj, prop) - - 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 = 0.0 - if obj.UseComp: - offset = self.radius + obj.OffsetExtra.Value - if obj.Side == 'Inside': - offset = 0 - offset - if isHole: - offset = 0 - offset - params['Offset'] = offset - - jointype = ['Round', 'Square', 'Miter'] - params['JoinType'] = jointype.index(obj.JoinType) - - if obj.JoinType == 'Miter': - params['MiterLimit'] = obj.MiterLimit - - 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 - - if not obj.UseComp: - if direction == 'CCW': - params['orientation'] = 1 - else: - params['orientation'] = 0 - - return params - - def areaOpUseProjection(self, obj): - '''areaOpUseProjection(obj) ... returns True''' - return True - - def areaOpSetDefaultValues(self, obj, job): - '''areaOpSetDefaultValues(obj, job) ... sets default values. - Do not overwrite.''' - obj.Side = "Outside" - obj.OffsetExtra = 0.0 - obj.Direction = "CW" - obj.UseComp = True - obj.JoinType = "Round" - obj.MiterLimit = 0.1 - - -def SetupProperties(): - setup = PathAreaOp.SetupProperties() - setup.append('Side') - setup.append('OffsetExtra') - setup.append('Direction') - setup.append('UseComp') - setup.append('JoinType') - setup.append('MiterLimit') - return setup diff --git a/src/Mod/Path/PathScripts/PathProfileBaseGui.py b/src/Mod/Path/PathScripts/PathProfileBaseGui.py deleted file mode 100644 index 350bef44fc..0000000000 --- a/src/Mod/Path/PathScripts/PathProfileBaseGui.py +++ /dev/null @@ -1,137 +0,0 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * * -# * 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 FreeCADGui -import PathScripts.PathGui as PathGui -import PathScripts.PathOpGui as PathOpGui - -from PySide import QtCore - -__title__ = "Path Profile Operation Base UI" -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Base page controller for profile operations." - -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - -FeatureSide = 0x01 -FeatureProcessing = 0x02 - -class TaskPanelOpPage(PathOpGui.TaskPanelPage): - '''Base class for profile operation page controllers. Two sub features are - support - FeatureSide ... Is the Side property exposed in the UI - FeatureProcessing ... Are the processing check boxes supported by the operation - ''' - - def profileFeatures(self): - '''profileFeatures() ... return which of the optional profile features are supported. - Currently two features are supported: - FeatureSide ... Is the Side property exposed in the UI - FeatureProcessing ... Are the processing check boxes supported by the operation - Must be overwritten by subclasses.''' - - def getForm(self): - '''getForm() ... returns UI customized according to profileFeatures()''' - form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui") - - if not FeatureSide & self.profileFeatures(): - form.cutSide.hide() - form.cutSideLabel.hide() - - if not FeatureProcessing & self.profileFeatures(): - form.processCircles.hide() - form.processHoles.hide() - form.processPerimeter.hide() - - return form - - def getFields(self, obj): - '''getFields(obj) ... transfers values from UI to obj's proprties''' - PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) - if obj.UseComp != self.form.useCompensation.isChecked(): - obj.UseComp = self.form.useCompensation.isChecked() - if obj.UseStartPoint != self.form.useStartPoint.isChecked(): - obj.UseStartPoint = self.form.useStartPoint.isChecked() - if obj.Direction != str(self.form.direction.currentText()): - obj.Direction = str(self.form.direction.currentText()) - if obj.EnableRotation != str(self.form.enableRotation.currentText()): - obj.EnableRotation = str(self.form.enableRotation.currentText()) - - self.updateToolController(obj, self.form.toolController) - self.updateCoolant(obj, self.form.coolantController) - - if FeatureSide & self.profileFeatures(): - if obj.Side != str(self.form.cutSide.currentText()): - obj.Side = str(self.form.cutSide.currentText()) - - if FeatureProcessing & self.profileFeatures(): - if obj.processHoles != self.form.processHoles.isChecked(): - obj.processHoles = self.form.processHoles.isChecked() - if obj.processPerimeter != self.form.processPerimeter.isChecked(): - obj.processPerimeter = self.form.processPerimeter.isChecked() - if obj.processCircles != self.form.processCircles.isChecked(): - obj.processCircles = self.form.processCircles.isChecked() - - def setFields(self, obj): - '''setFields(obj) ... transfers obj's property values to UI''' - self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) - self.form.useCompensation.setChecked(obj.UseComp) - self.form.useStartPoint.setChecked(obj.UseStartPoint) - - self.selectInComboBox(obj.Direction, self.form.direction) - self.setupToolController(obj, self.form.toolController) - self.setupCoolant(obj, self.form.coolantController) - self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) - - if FeatureSide & self.profileFeatures(): - self.selectInComboBox(obj.Side, self.form.cutSide) - - if FeatureProcessing & self.profileFeatures(): - self.form.processHoles.setChecked(obj.processHoles) - self.form.processPerimeter.setChecked(obj.processPerimeter) - self.form.processCircles.setChecked(obj.processCircles) - - def getSignalsForUpdate(self, obj): - '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' - signals = [] - signals.append(self.form.direction.currentIndexChanged) - signals.append(self.form.useCompensation.clicked) - signals.append(self.form.useStartPoint.clicked) - signals.append(self.form.extraOffset.editingFinished) - signals.append(self.form.toolController.currentIndexChanged) - signals.append(self.form.coolantController.currentIndexChanged) - signals.append(self.form.enableRotation.currentIndexChanged) - - if FeatureSide & self.profileFeatures(): - signals.append(self.form.cutSide.currentIndexChanged) - - if FeatureProcessing & self.profileFeatures(): - signals.append(self.form.processHoles.clicked) - signals.append(self.form.processPerimeter.clicked) - signals.append(self.form.processCircles.clicked) - - return signals diff --git a/src/Mod/Path/PathScripts/PathProfileContour.py b/src/Mod/Path/PathScripts/PathProfileContour.py index 5aaeb8c56d..dfaf43dcb2 100644 --- a/src/Mod/Path/PathScripts/PathProfileContour.py +++ b/src/Mod/Path/PathScripts/PathProfileContour.py @@ -21,100 +21,32 @@ # * USA * # * * # *************************************************************************** - -from __future__ import print_function +# * Major modifications: 2020 Russell Johnson * import FreeCAD -import Path -import PathScripts.PathProfileBase as PathProfileBase -import PathScripts.PathLog as PathLog +import PathScripts.PathProfile as PathProfile -from PathScripts import PathUtils -from PySide import QtCore -# lazily loaded modules -from lazy_loader.lazy_loader import LazyLoader -ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') -Part = LazyLoader('Part', globals(), 'Part') - -FreeCAD.setLogLevel('Path.Area', 0) - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -#PathLog.trackModule(PathLog.thisModule()) - -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - -__title__ = "Path Contour Operation" +__title__ = "Path Contour Operation (depreciated)" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" -__doc__ = "Implementation of the Contour operation." +__doc__ = "Implementation of the Contour operation (depreciated)." -class ObjectContour(PathProfileBase.ObjectProfile): - '''Proxy object for Contour operations.''' +class ObjectContour(PathProfile.ObjectProfile): + '''Psuedo class for Profile operation, + allowing for backward compatibility with pre-existing "Contour" operations.''' + pass +# Eclass - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - - def areaOpFeatures(self, obj): - '''areaOpFeatures(obj) ... returns 0, Contour only requires the base profile features.''' - return 0 - - def initAreaOp(self, obj): - '''initAreaOp(obj) ... call super's implementation and hide Side property.''' - self.baseObject().initAreaOp(obj) - obj.setEditorMode('Side', 2) # it's always outside - - def areaOpOnDocumentRestored(self, obj): - obj.setEditorMode('Side', 2) # it's always outside - - def areaOpSetDefaultValues(self, obj, job): - '''areaOpSetDefaultValues(obj, job) ... call super's implementation and set Side="Outside".''' - self.baseObject().areaOpSetDefaultValues(obj, job) - obj.Side = 'Outside' - - def areaOpShapes(self, obj): - '''areaOpShapes(obj) ... return envelope over the job's Base.Shape or all Arch.Panel shapes.''' - if obj.UseComp: - self.commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")")) - else: - self.commandlist.append(Path.Command("(Uncompensated Tool Path)")) - - isPanel = False - if 1 == len(self.model) and hasattr(self.model[0], "Proxy"): - if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet - panel = self.model[0] - isPanel = True - panel.Proxy.execute(panel) - shapes = panel.Proxy.getOutlines(panel, transform=True) - for shape in shapes: - f = Part.makeFace([shape], 'Part::FaceMakerSimple') - thickness = panel.Group[0].Source.Thickness - return [(f.extrude(FreeCAD.Vector(0, 0, thickness)), False)] - - if not isPanel: - return [(PathUtils.getEnvelope(partshape=base.Shape, subshape=None, depthparams=self.depthparams), False) for base in self.model if hasattr(base, 'Shape')] - - def areaOpAreaParams(self, obj, isHole): - params = self.baseObject().areaOpAreaParams(obj, isHole) - params['Coplanar'] = 2 - return params - - def opUpdateDepths(self, obj): - obj.OpStartDepth = obj.OpStockZMax - obj.OpFinalDepth = obj.OpStockZMin def SetupProperties(): - return [p for p in PathProfileBase.SetupProperties() if p != 'Side'] + return PathProfile.SetupProperties() -def Create(name, obj = None): - '''Create(name) ... Creates and returns a Contour operation.''' + +def Create(name, obj=None): + '''Create(name) ... Creates and returns a Profile operation.''' if obj is None: - obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) + obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Proxy = ObjectContour(obj, name) return obj - diff --git a/src/Mod/Path/PathScripts/PathProfileContourGui.py b/src/Mod/Path/PathScripts/PathProfileContourGui.py index 6c9321ac6c..74277be7d2 100644 --- a/src/Mod/Path/PathScripts/PathProfileContourGui.py +++ b/src/Mod/Path/PathScripts/PathProfileContourGui.py @@ -21,32 +21,34 @@ # * USA * # * * # *************************************************************************** +# * Major modifications: 2020 Russell Johnson * import FreeCAD import PathScripts.PathOpGui as PathOpGui -import PathScripts.PathProfileBaseGui as PathProfileBaseGui -import PathScripts.PathProfileContour as PathProfileContour - +import PathScripts.PathProfile as PathProfile +import PathScripts.PathProfileGui as PathProfileGui from PySide import QtCore -__title__ = "Path Contour Operation UI" + +__title__ = "Path Contour Operation UI (depreciated)" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" -__doc__ = "Contour operation page controller and command implementation." +__doc__ = "Contour operation page controller and command implementation (depreciated)." -class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage): - '''Page controller for the contour operation UI.''' - def profileFeatures(self): - '''profileFeatues() ... return 0 - profile doesn't support any of the optional UI features.''' - return 0 +class TaskPanelOpPage(PathProfileGui.TaskPanelOpPage): + '''Psuedo page controller class for Profile operation, + allowing for backward compatibility with pre-existing "Contour" operations.''' + pass +# Eclass -Command = PathOpGui.SetupOperation('Contour', - PathProfileContour.Create, + +Command = PathOpGui.SetupOperation('Profile', + PathProfile.Create, TaskPanelOpPage, 'Path-Contour', - QtCore.QT_TRANSLATE_NOOP("PathProfileContour", "Contour"), - QtCore.QT_TRANSLATE_NOOP("PathProfileContour", "Creates a Contour Path for the Base Object "), - PathProfileContour.SetupProperties) + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), + PathProfile.SetupProperties) FreeCAD.Console.PrintLog("Loading PathProfileContourGui... done\n") diff --git a/src/Mod/Path/PathScripts/PathProfileEdges.py b/src/Mod/Path/PathScripts/PathProfileEdges.py index b266b53ea4..e23668c4ba 100644 --- a/src/Mod/Path/PathScripts/PathProfileEdges.py +++ b/src/Mod/Path/PathScripts/PathProfileEdges.py @@ -21,937 +21,32 @@ # * USA * # * * # *************************************************************************** +# * Major modifications: 2020 Russell Johnson * import FreeCAD -import Path -import PathScripts.PathLog as PathLog -import PathScripts.PathOp as PathOp -import PathScripts.PathProfileBase as PathProfileBase -import PathScripts.PathUtils as PathUtils - -import math -import PySide - -# lazily loaded modules -from lazy_loader.lazy_loader import LazyLoader -Part = LazyLoader('Part', globals(), 'Part') -DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) -# PathLog.trackModule(PathLog.thisModule()) +import PathScripts.PathProfile as PathProfile -# Qt translation handling -def translate(context, text, disambig=None): - return PySide.QtCore.QCoreApplication.translate(context, text, disambig) - - -__title__ = "Path Profile Edges Operation" +__title__ = "Path Profile Edges Operation (depreciated)" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" -__doc__ = "Path Profile operation based on edges." +__doc__ = "Path Profile operation based on edges (depreciated)." __contributors__ = "russ4262 (Russell Johnson)" -class ObjectProfile(PathProfileBase.ObjectProfile): - '''Proxy object for Profile operations based on edges.''' - - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - - def areaOpFeatures(self, obj): - '''areaOpFeatures(obj) ... add support for edge base geometry.''' - return PathOp.FeatureBaseEdges - - def areaOpShapes(self, obj): - '''areaOpShapes(obj) ... returns envelope for all wires formed by the base edges.''' - PathLog.track() - - inaccessible = translate('PathProfileEdges', 'The selected edge(s) are inaccessible. If multiple, re-ordering selection might work.') - if PathLog.getLevel(PathLog.thisModule()) == 4: - self.tmpGrp = FreeCAD.ActiveDocument.addObject('App::DocumentObjectGroup', 'tmpDebugGrp') - tmpGrpNm = self.tmpGrp.Name - self.JOB = PathUtils.findParentJob(obj) - - self.offsetExtra = abs(obj.OffsetExtra.Value) - - 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)")) - - shapes = [] - if obj.Base: - basewires = [] - - zMin = None - for b in obj.Base: - edgelist = [] - for sub in b[1]: - edgelist.append(getattr(b[0].Shape, sub)) - basewires.append((b[0], DraftGeomUtils.findWires(edgelist))) - if zMin is None or b[0].Shape.BoundBox.ZMin < zMin: - zMin = b[0].Shape.BoundBox.ZMin - - PathLog.debug('PathProfileEdges areaOpShapes():: len(basewires) is {}'.format(len(basewires))) - for base, wires in basewires: - for wire in wires: - if wire.isClosed() is True: - # 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 = origWire.Wires[0] - if f is not False: - # shift the compound to the bottom of the base object for proper sectioning - zShift = zMin - f.BoundBox.ZMin - newPlace = FreeCAD.Placement(FreeCAD.Vector(0, 0, zShift), f.Placement.Rotation) - f.Placement = newPlace - env = PathUtils.getEnvelope(base.Shape, subshape=f, depthparams=self.depthparams) - shapes.append((env, False)) - else: - PathLog.error(inaccessible) - else: - if self.JOB.GeometryTolerance.Value == 0.0: - msg = self.JOB.Label + '.GeometryTolerance = 0.0.' - msg += translate('PathProfileEdges', 'Please set to an acceptable value greater than zero.') - PathLog.error(msg) - else: - cutWireObjs = False - flattened = self._flattenWire(obj, wire, obj.FinalDepth.Value) - if flattened: - (origWire, flatWire) = flattened - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpFlatWire') - os.Shape = flatWire - os.purgeTouched() - self.tmpGrp.addObject(os) - cutShp = self._getCutAreaCrossSection(obj, base, origWire, flatWire) - if cutShp is not False: - cutWireObjs = self._extractPathWire(obj, base, flatWire, cutShp) - - if cutWireObjs is not False: - for cW in cutWireObjs: - shapes.append((cW, False)) - self.profileEdgesIsOpen = True - else: - PathLog.error(inaccessible) - else: - PathLog.error(inaccessible) - - # Delete the temporary objects - if PathLog.getLevel(PathLog.thisModule()) == 4: - if FreeCAD.GuiUp: - import FreeCADGui - FreeCADGui.ActiveDocument.getObject(tmpGrpNm).Visibility = False - self.tmpGrp.purgeTouched() - - return shapes - - def _flattenWire(self, obj, wire, trgtDep): - '''_flattenWire(obj, wire)... Return a flattened version of the wire''' - PathLog.debug('_flattenWire()') - wBB = wire.BoundBox - - if wBB.ZLength > 0.0: - PathLog.debug('Wire is not horizontally co-planar. Flattening it.') - - # Extrude non-horizontal wire - extFwdLen = wBB.ZLength * 2.2 - 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): - PathLog.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 = translate('PathProfile', - 'For open edges, select top edge and set Final Depth manually.') - FreeCAD.Console.PrintError(msg + '\n') - return False - 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) - - # Identify endpoints connecting circle center and diameter - vectDist = pe.sub(pb) - diam = vectDist.Length - cntr = vectDist.multiply(0.5).add(pb) - R = diam / 2 - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - # 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) - if PathLog.getLevel(PathLog.thisModule()) == 4: - CA = FCAD.addObject('Part::Feature', 'tmpCutArea') - CA.Shape = cutArea - CA.recompute() - CA.purgeTouched() - self.tmpGrp.addObject(CA) - - - # Get top and bottom faces of cut area (CA), and combine faces when necessary - topFc = list() - botFc = list() - 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: - PathLog.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: - PathLog.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: - PathLog.debug('Cutting on Ext side.') - self.cutSide = 'E' - self.cutSideTags = eTAG - tagCOM = begExt.CenterOfMass - else: - PathLog.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 - fcShp = workShp - wire = origWire - WS = workShp.Wires - lenWS = len(WS) - if lenWS < 3: - wi = 0 - 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: - PathLog.error('The cut area cross-section wire does not coincide with selected edge. Wires[] index is None.') - return False - else: - PathLog.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: - PathLog.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) - PathLog.debug('isReady {}.'.format(isReady)) - - if isReady is False: - PathLog.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: - PathLog.debug('offset area is less than minArea of {}.'.format(minArea)) - PathLog.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) - return cutShp - - def _checkTagIntersection(self, iTAG, eTAG, cutSide, tstObj): - # 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: - PathLog.debug('Cutting on Ext side.') - if cutSide == 'E': - return True - else: - PathLog.debug('Cutting on Int side.') - if cutSide == 'I': - return True - return False - - def _extractPathWire(self, obj, base, flatWire, cutShp): - PathLog.debug('_extractPathWire()') - - subLoops = list() - rtnWIRES = list() - osWrIdxs = list() - subDistFactor = 1.0 # Raise to include sub wires at greater distance from original - fdv = obj.FinalDepth.Value - wire = flatWire - lstVrtIdx = len(wire.Vertexes) - 1 - lstVrt = wire.Vertexes[lstVrtIdx] - frstVrt = wire.Vertexes[0] - cent0 = FreeCAD.Vector(frstVrt.X, frstVrt.Y, fdv) - cent1 = FreeCAD.Vector(lstVrt.X, lstVrt.Y, fdv) - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - # Calculate offset shape, containing cut region - ofstShp = self._extractFaceOffset(obj, cutShp, False) - - # CHECK for ZERO area of offset shape - try: - osArea = ofstShp.Area - except Exception as ee: - PathLog.error('No area to offset shape returned.') - return False - - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpOffsetShape') - os.Shape = ofstShp - os.recompute() - os.purgeTouched() - self.tmpGrp.addObject(os) - - numOSWires = len(ofstShp.Wires) - for w in range(0, numOSWires): - osWrIdxs.append(w) - - # Identify two vertexes for dividing offset loop - NEAR0 = self._findNearestVertex(ofstShp, cent0) - min0i = 0 - min0 = NEAR0[0][4] - for n in range(0, len(NEAR0)): - N = NEAR0[n] - if N[4] < min0: - min0 = N[4] - min0i = n - (w0, vi0, pnt0, vrt0, d0) = NEAR0[0] # min0i - if PathLog.getLevel(PathLog.thisModule()) == 4: - near0 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear0') - near0.Shape = Part.makeLine(cent0, pnt0) - near0.recompute() - near0.purgeTouched() - self.tmpGrp.addObject(near0) - - NEAR1 = self._findNearestVertex(ofstShp, cent1) - min1i = 0 - min1 = NEAR1[0][4] - for n in range(0, len(NEAR1)): - N = NEAR1[n] - if N[4] < min1: - min1 = N[4] - min1i = n - (w1, vi1, pnt1, vrt1, d1) = NEAR1[0] # min1i - if PathLog.getLevel(PathLog.thisModule()) == 4: - near1 = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmpNear1') - near1.Shape = Part.makeLine(cent1, pnt1) - near1.recompute() - near1.purgeTouched() - self.tmpGrp.addObject(near1) - - if w0 != w1: - PathLog.warning('Offset wire endpoint indexes are not equal - w0, w1: {}, {}'.format(w0, w1)) - - if PathLog.getLevel(PathLog.thisModule()) == 4: - PathLog.debug('min0i is {}.'.format(min0i)) - PathLog.debug('min1i is {}.'.format(min1i)) - PathLog.debug('NEAR0[{}] is {}.'.format(w0, NEAR0[w0])) - PathLog.debug('NEAR1[{}] is {}.'.format(w1, NEAR1[w1])) - PathLog.debug('NEAR0 is {}.'.format(NEAR0)) - PathLog.debug('NEAR1 is {}.'.format(NEAR1)) - - mainWire = ofstShp.Wires[w0] - - # Check for additional closed loops in offset wire by checking distance to iTAG or eTAG elements - if numOSWires > 1: - # check all wires for proximity(children) to intersection tags - tagsComList = list() - for T in self.cutSideTags.Faces: - tcom = T.CenterOfMass - tv = FreeCAD.Vector(tcom.x, tcom.y, 0.0) - tagsComList.append(tv) - subDist = self.ofstRadius * subDistFactor - for w in osWrIdxs: - if w != w0: - cutSub = False - VTXS = ofstShp.Wires[w].Vertexes - for V in VTXS: - v = FreeCAD.Vector(V.X, V.Y, 0.0) - for t in tagsComList: - if t.sub(v).Length < subDist: - cutSub = True - break - if cutSub is True: - break - if cutSub is True: - sub = Part.Wire(Part.__sortEdges__(ofstShp.Wires[w].Edges)) - subLoops.append(sub) - # Eif - - # Break offset loop into two wires - one of which is the desired profile path wire. - (edgeIdxs0, edgeIdxs1) = self._separateWireAtVertexes(mainWire, mainWire.Vertexes[vi0], mainWire.Vertexes[vi1]) - edgs0 = list() - edgs1 = list() - for e in edgeIdxs0: - edgs0.append(mainWire.Edges[e]) - for e in edgeIdxs1: - edgs1.append(mainWire.Edges[e]) - part0 = Part.Wire(Part.__sortEdges__(edgs0)) - part1 = Part.Wire(Part.__sortEdges__(edgs1)) - - # Determine which part is nearest original edge(s) - distToPart0 = self._distMidToMid(wire.Wires[0], part0.Wires[0]) - distToPart1 = self._distMidToMid(wire.Wires[0], part1.Wires[0]) - if distToPart0 < distToPart1: - rtnWIRES.append(part0) - else: - rtnWIRES.append(part1) - rtnWIRES.extend(subLoops) - - return rtnWIRES - - def _extractFaceOffset(self, obj, fcShape, isHole): - '''_extractFaceOffset(obj, fcShape, isHole) ... internal function. - Original _buildPathArea() version copied from PathAreaOp.py module. This version is modified. - Adjustments made based on notes by @sliptonic - https://github.com/sliptonic/FreeCAD/wiki/PathArea-notes.''' - PathLog.debug('_extractFaceOffset()') - - areaParams = {} - JOB = PathUtils.findParentJob(obj) - tolrnc = JOB.GeometryTolerance.Value - if self.useComp is True: - offset = self.ofstRadius # + tolrnc - else: - offset = self.offsetExtra # + tolrnc - - if isHole is False: - offset = 0 - offset - - areaParams['Offset'] = offset - areaParams['Fill'] = 1 - areaParams['Coplanar'] = 0 - areaParams['SectionCount'] = 1 # -1 = full(all per depthparams??) sections - areaParams['Reorient'] = True - areaParams['OpenMode'] = 0 - areaParams['MaxArcPoints'] = 400 # 400 - areaParams['Project'] = True - # areaParams['JoinType'] = 1 - - area = Path.Area() # Create instance of Area() class object - area.setPlane(PathUtils.makeWorkplane(fcShape)) # Set working plane - area.add(fcShape) # obj.Shape to use for extracting offset - area.setParams(**areaParams) # set parameters - - return area.getShape() - - def _findNearestVertex(self, shape, point): - PathLog.debug('_findNearestVertex()') - PT = FreeCAD.Vector(point.x, point.y, 0.0) - - def sortDist(tup): - return tup[4] - - PNTS = list() - 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): - PathLog.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) - - lenE = len(wire.Edges) - FLGS = list() - for e in range(0, lenE): - FLGS.append(0) - - chk4 = False - for e in range(0, lenE): - 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 - PathLog.debug('_separateWireAtVertexes() FLGS: \n{}'.format(FLGS)) - - PRE = list() - POST = list() - IDXS = list() - IDX1 = list() - IDX2 = list() - for e in range(0, lenE): - 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 - begFlg = False - 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 - endIdx = None - for e in range(begIdx + 1, lenE + 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 begFlg is False: - begFlg = True - else: - begIdx = e - break - # find first 3 edge and group all first wire edges - endIdx = None - for e in range(begIdx, lenE + 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 - - if PathLog.getLevel(PathLog.thisModule()) != 4: - PathLog.debug('grps[0]: {}'.format(grps[0])) - PathLog.debug('grps[1]: {}'.format(grps[1])) - PathLog.debug('wireIdxs[0]: {}'.format(wireIdxs[0])) - PathLog.debug('wireIdxs[1]: {}'.format(wireIdxs[1])) - PathLog.debug('PRE: {}'.format(PRE)) - PathLog.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.''' - # Create cross-section of shape and translate - wires = list() - 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): - 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): - # Create circular probe tags around perimiter of wire - extTags = list() - intTags = list() - 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, 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 - cp1 = E.valueAt(E.getParameterByLength(d - spc)) - cp2 = E.valueAt(E.getParameterByLength(d + spc)) - (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): - 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)) - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - # 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): - rad = self.radius - ofstRad = self.ofstRadius - extra = self.radius / 10 - - pl = FreeCAD.Placement() - pl.Rotation = FreeCAD.Rotation(FreeCAD.Vector(0, 0, 1), 0) - pl.Base = FreeCAD.Vector(0, 0, 0) - - E = FreeCAD.Vector(pB.x, pB.y, 0) # endpoint - C = FreeCAD.Vector(pA.x, pA.y, 0) # checkpoint - lenEC = E.sub(C).Length - - if self.useComp is True or (self.useComp is False and self.offsetExtra != 0): - # 'L' stop shape and edge legend - # --1-- - # | | - # 2 6 - # | | - # | ----5----| - # | 4 - # -----3-------| - # positive dist in _makePerp2DVector() is CCW rotation - p1 = E - if sType == 'BEG': - p2 = self._makePerp2DVector(C, E, -0.25) # E1 - p3 = self._makePerp2DVector(p1, p2, ofstRad + 1 + extra) # E2 - p4 = self._makePerp2DVector(p2, p3, 0.25 + ofstRad + extra) # E3 - p5 = self._makePerp2DVector(p3, p4, 1 + extra) # E4 - p6 = self._makePerp2DVector(p4, p5, ofstRad + extra) # E5 - elif sType == 'END': - p2 = self._makePerp2DVector(C, E, 0.25) # E1 - p3 = self._makePerp2DVector(p1, p2, -1 * (ofstRad + 1 + extra)) # E2 - p4 = self._makePerp2DVector(p2, p3, -1 * (0.25 + ofstRad + extra)) # E3 - p5 = self._makePerp2DVector(p3, p4, -1 * (1 + 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 legend - # : - # |----2-------| - # 3 1 - # |-----4------| - # positive dist in _makePerp2DVector() is CCW rotation - p1 = E - if sType == 'BEG': - p2 = self._makePerp2DVector(C, E, -1 * (0.25 + abs(self.offsetExtra))) # left, 0.25 - p3 = self._makePerp2DVector(p1, p2, 0.25 + abs(self.offsetExtra)) - p4 = self._makePerp2DVector(p2, p3, (0.5 + abs(self.offsetExtra))) # FIRST POINT - p5 = self._makePerp2DVector(p3, p4, 0.25 + abs(self.offsetExtra)) # E1 SECOND - elif sType == 'END': - p2 = self._makePerp2DVector(C, E, (0.25 + abs(self.offsetExtra))) # left, 0.25 - p3 = self._makePerp2DVector(p1, p2, -1 * (0.25 + abs(self.offsetExtra))) - p4 = self._makePerp2DVector(p2, p3, -1 * (0.5 + abs(self.offsetExtra))) # FIRST POINT - p5 = self._makePerp2DVector(p3, p4, -1 * (0.25 + 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) - if PathLog.getLevel(PathLog.thisModule()) == 4: - os = FreeCAD.ActiveDocument.addObject('Part::Feature', 'tmp' + lbl) - os.Shape = face - os.recompute() - os.purgeTouched() - self.tmpGrp.addObject(os) - - 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 range(0, len(wire.Edges)): - E = wire.Edges[e] - 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 +class ObjectProfile(PathProfile.ObjectProfile): + '''Psuedo class for Profile operation, + allowing for backward compatibility with pre-existing "Profile Edges" operations.''' + pass +# Eclass def SetupProperties(): - return PathProfileBase.SetupProperties() + return PathProfile.SetupProperties() -def Create(name, obj = None): - '''Create(name) ... Creates and returns a Profile based on edges operation.''' +def Create(name, obj=None): + '''Create(name) ... Creates and returns a Profile operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Proxy = ObjectProfile(obj, name) diff --git a/src/Mod/Path/PathScripts/PathProfileEdgesGui.py b/src/Mod/Path/PathScripts/PathProfileEdgesGui.py index 205af01bed..9f156d5d71 100644 --- a/src/Mod/Path/PathScripts/PathProfileEdgesGui.py +++ b/src/Mod/Path/PathScripts/PathProfileEdgesGui.py @@ -21,33 +21,34 @@ # * USA * # * * # *************************************************************************** +# * Major modifications: 2020 Russell Johnson * import FreeCAD import PathScripts.PathOpGui as PathOpGui -import PathScripts.PathProfileBaseGui as PathProfileBaseGui -import PathScripts.PathProfileEdges as PathProfileEdges - +import PathScripts.PathProfile as PathProfile +import PathScripts.PathProfileGui as PathProfileGui from PySide import QtCore -__title__ = "Path Profile based on edges Operation UI" + +__title__ = "Path Profile Edges Operation UI (depreciated)" __author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" -__doc__ = "Profile based on edges operation page controller and command implementation." +__doc__ = "Profile Edges operation page controller and command implementation (depreciated)." -class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage): - '''Page controller for profile based on edges operation.''' - def profileFeatures(self): - '''profileFeatures() ... return FeatureSide - See PathProfileBaseGui.py for details.''' - return PathProfileBaseGui.FeatureSide +class TaskPanelOpPage(PathProfileGui.TaskPanelOpPage): + '''Psuedo page controller class for Profile operation, + allowing for backward compatibility with pre-existing "Profile Edges" operations.''' + pass +# Eclass -Command = PathOpGui.SetupOperation('Profile Edges', - PathProfileEdges.Create, + +Command = PathOpGui.SetupOperation('Profile', + PathProfile.Create, TaskPanelOpPage, - 'Path-Profile-Edges', - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Edge Profile"), - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on edges"), - PathProfileEdges.SetupProperties) + 'Path-Contour', + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), + PathProfile.SetupProperties) FreeCAD.Console.PrintLog("Loading PathProfileEdgesGui... done\n") diff --git a/src/Mod/Path/PathScripts/PathProfileFaces.py b/src/Mod/Path/PathScripts/PathProfileFaces.py index 281d848699..51845ca329 100644 --- a/src/Mod/Path/PathScripts/PathProfileFaces.py +++ b/src/Mod/Path/PathScripts/PathProfileFaces.py @@ -22,328 +22,32 @@ # * USA * # * * # *************************************************************************** +# * Major modifications: 2020 Russell Johnson * import FreeCAD -import Path -import PathScripts.PathLog as PathLog -import PathScripts.PathOp as PathOp -import PathScripts.PathProfileBase as PathProfileBase -import PathScripts.PathUtils as PathUtils -import numpy +import PathScripts.PathProfile as PathProfile -from PySide import QtCore -# lazily loaded modules -from lazy_loader.lazy_loader import LazyLoader -ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') -Part = LazyLoader('Part', globals(), 'Part') - -__title__ = "Path Profile Faces Operation" -__author__ = "sliptonic (Brad Collette), Schildkroet" +__title__ = "Path Profile Faces Operation (depreciated)" +__author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" -__doc__ = "Path Profile operation based on faces." - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +__doc__ = "Path Profile operation based on faces (depreciated)." +__contributors__ = "Schildkroet" -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -class ObjectProfile(PathProfileBase.ObjectProfile): - '''Proxy object for Profile operations based on faces.''' - - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - - def areaOpFeatures(self, obj): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - # return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureRotation - return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels - - def initAreaOp(self, obj): - '''initAreaOp(obj) ... adds properties for hole, circle and perimeter processing.''' - # Face specific Properties - obj.addProperty("App::PropertyBool", "processHoles", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile holes as well as the outline")) - obj.addProperty("App::PropertyBool", "processPerimeter", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile the outline")) - obj.addProperty("App::PropertyBool", "processCircles", "Profile", QtCore.QT_TRANSLATE_NOOP("App::Property", "Profile round holes")) - - if not hasattr(obj, 'HandleMultipleFeatures'): - obj.addProperty('App::PropertyEnumeration', 'HandleMultipleFeatures', 'Profile', QtCore.QT_TRANSLATE_NOOP('PathPocket', 'Choose how to process multiple Base Geometry features.')) - - obj.HandleMultipleFeatures = ['Collectively', 'Individually'] - - self.initRotationOp(obj) - self.baseObject().initAreaOp(obj) - - def initRotationOp(self, obj): - '''initRotationOp(obj) ... setup receiver for rotation''' - if not hasattr(obj, 'ReverseDirection'): - obj.addProperty('App::PropertyBool', 'ReverseDirection', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Reverse direction of pocket operation.')) - if not hasattr(obj, 'InverseAngle'): - obj.addProperty('App::PropertyBool', 'InverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Inverse the angle. Example: -22.5 -> 22.5 degrees.')) - if not hasattr(obj, 'AttemptInverseAngle'): - obj.addProperty('App::PropertyBool', 'AttemptInverseAngle', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Attempt the inverse angle for face access if original rotation fails.')) - if not hasattr(obj, 'LimitDepthToFace'): - obj.addProperty('App::PropertyBool', 'LimitDepthToFace', 'Rotation', QtCore.QT_TRANSLATE_NOOP('App::Property', 'Enforce the Z-depth of the selected face as the lowest value for final depth. Higher user values will be observed.')) - - def extraOpOnChanged(self, obj, prop): - '''extraOpOnChanged(obj, porp) ... process operation specific changes to properties.''' - if prop == 'EnableRotation': - self.setOpEditorProperties(obj) - - def setOpEditorProperties(self, obj): - if obj.EnableRotation == 'Off': - obj.setEditorMode('ReverseDirection', 2) - obj.setEditorMode('InverseAngle', 2) - obj.setEditorMode('AttemptInverseAngle', 2) - obj.setEditorMode('LimitDepthToFace', 2) - else: - obj.setEditorMode('ReverseDirection', 0) - obj.setEditorMode('InverseAngle', 0) - obj.setEditorMode('AttemptInverseAngle', 0) - obj.setEditorMode('LimitDepthToFace', 0) - - def areaOpShapes(self, obj): - '''areaOpShapes(obj) ... returns envelope for all base shapes or wires for Arch.Panels.''' - PathLog.track() - - if obj.UseComp: - self.commandlist.append(Path.Command("(Compensated Tool Path. Diameter: " + str(self.radius * 2) + ")")) - else: - self.commandlist.append(Path.Command("(Uncompensated Tool Path)")) - - shapes = [] - self.profileshape = [] # pylint: disable=attribute-defined-outside-init - - baseSubsTuples = [] - subCount = 0 - allTuples = [] - - if obj.Base: # The user has selected subobjects from the base. Process each. - if obj.EnableRotation != 'Off': - for p in range(0, len(obj.Base)): - (base, subsList) = obj.Base[p] - for sub in subsList: - subCount += 1 - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - rtn = False - (norm, surf) = self.getFaceNormAndSurf(shape) - (rtn, angle, axis, praInfo) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("initial faceRotationAnalysis: {}".format(praInfo)) - if rtn is True: - (clnBase, angle, clnStock, tag) = self.applyRotationalAnalysis(obj, base, angle, axis, subCount) - # Verify faces are correctly oriented - InverseAngle might be necessary - faceIA = getattr(clnBase.Shape, sub) - (norm, surf) = self.getFaceNormAndSurf(faceIA) - (rtn, praAngle, praAxis, praInfo2) = self.faceRotationAnalysis(obj, norm, surf) # pylint: disable=unused-variable - PathLog.debug("follow-up faceRotationAnalysis: {}".format(praInfo2)) - - if abs(praAngle) == 180.0: - rtn = False - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 1 is False') - angle -= 180.0 - - if rtn is True: - PathLog.debug(translate("Path", "Face appears misaligned after initial rotation.")) - if obj.InverseAngle is False: - if obj.AttemptInverseAngle is True: - (clnBase, clnStock, angle) = self.applyInverseAngle(obj, clnBase, clnStock, axis, angle) - else: - msg = translate("Path", "Consider toggling the 'InverseAngle' property and recomputing.") - PathLog.warning(msg) - - if self.isFaceUp(clnBase, faceIA) is False: - PathLog.debug('isFaceUp 2 is False') - angle += 180.0 - else: - PathLog.debug(' isFaceUp') - - else: - PathLog.debug("Face appears to be oriented correctly.") - - if angle < 0.0: - angle += 360.0 - - tup = clnBase, sub, tag, angle, axis, clnStock - else: - if self.warnDisabledAxis(obj, axis) is False: - PathLog.debug(str(sub) + ": No rotation used") - axis = 'X' - angle = 0.0 - tag = base.Name + '_' + axis + str(angle).replace('.', '_') - stock = PathUtils.findParentJob(obj).Stock - tup = base, sub, tag, angle, axis, stock - - allTuples.append(tup) - - if subCount > 1: - msg = translate('Path', "Multiple faces in Base Geometry.") + " " - msg += translate('Path', "Depth settings will be applied to all faces.") - PathLog.warning(msg) - - (Tags, Grps) = self.sortTuplesByIndex(allTuples, 2) # return (TagList, GroupList) - subList = [] - for o in range(0, len(Tags)): - subList = [] - for (base, sub, tag, angle, axis, stock) in Grps[o]: - subList.append(sub) - - pair = base, subList, angle, axis, stock - baseSubsTuples.append(pair) - # Efor - else: - PathLog.debug(translate("Path", "EnableRotation property is 'Off'.")) - stock = PathUtils.findParentJob(obj).Stock - for (base, subList) in obj.Base: - baseSubsTuples.append((base, subList, 0.0, 'X', stock)) - - # for base in obj.Base: - finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0 - for (base, subsList, angle, axis, stock) in baseSubsTuples: - holes = [] - faces = [] - faceDepths = [] - startDepths = [] - - for sub in subsList: - shape = getattr(base.Shape, sub) - if isinstance(shape, Part.Face): - faces.append(shape) - if numpy.isclose(abs(shape.normalAt(0, 0).z), 1): # horizontal face - for wire in shape.Wires[1:]: - holes.append((base.Shape, wire)) - - # Add face depth to list - faceDepths.append(shape.BoundBox.ZMin) - else: - ignoreSub = base.Name + '.' + sub - msg = translate('Path', "Found a selected object which is not a face. Ignoring: {}".format(ignoreSub)) - PathLog.error(msg) - FreeCAD.Console.PrintWarning(msg) - - # Set initial Start and Final Depths and recalculate depthparams - finDep = obj.FinalDepth.Value - strDep = obj.StartDepth.Value - if strDep > stock.Shape.BoundBox.ZMax: - strDep = stock.Shape.BoundBox.ZMax - - startDepths.append(strDep) - self.depthparams = self._customDepthParams(obj, strDep, finDep) - - for shape, wire in holes: - f = Part.makeFace(wire, 'Part::FaceMakerSimple') - drillable = PathUtils.isDrillable(shape, wire) - if (drillable and obj.processCircles) or (not drillable and obj.processHoles): - env = PathUtils.getEnvelope(shape, subshape=f, depthparams=self.depthparams) - tup = env, True, 'pathProfileFaces', angle, axis, strDep, finDep - shapes.append(tup) - - if len(faces) > 0: - profileshape = Part.makeCompound(faces) - self.profileshape.append(profileshape) - - if obj.processPerimeter: - if obj.HandleMultipleFeatures == 'Collectively': - custDepthparams = self.depthparams - - if obj.LimitDepthToFace is True and obj.EnableRotation != 'Off': - if profileshape.BoundBox.ZMin > obj.FinalDepth.Value: - finDep = profileshape.BoundBox.ZMin - envDepthparams = self._customDepthParams(obj, strDep + 0.5, finDep) # only an envelope - try: - # env = PathUtils.getEnvelope(base.Shape, subshape=profileshape, depthparams=envDepthparams) - env = PathUtils.getEnvelope(profileshape, depthparams=envDepthparams) - except Exception: # pylint: disable=broad-except - # PathUtils.getEnvelope() failed to return an object. - PathLog.error(translate('Path', 'Unable to create path for face(s).')) - else: - tup = env, False, 'pathProfileFaces', angle, axis, strDep, finDep - shapes.append(tup) - - elif obj.HandleMultipleFeatures == 'Individually': - for shape in faces: - # profShape = Part.makeCompound([shape]) - finalDep = obj.FinalDepth.Value - custDepthparams = self.depthparams - if obj.Side == 'Inside': - if finalDep < shape.BoundBox.ZMin: - # Recalculate depthparams - finalDep = shape.BoundBox.ZMin - custDepthparams = self._customDepthParams(obj, strDep + 0.5, finalDep) - - # env = PathUtils.getEnvelope(base.Shape, subshape=profShape, depthparams=custDepthparams) - env = PathUtils.getEnvelope(shape, depthparams=custDepthparams) - tup = env, False, 'pathProfileFaces', angle, axis, strDep, finalDep - shapes.append(tup) - - # Lower high Start Depth to top of Stock - startDepth = max(startDepths) - if obj.StartDepth.Value > startDepth: - obj.StartDepth.Value = startDepth - - else: # Try to build targets from the job base - if 1 == len(self.model): - if hasattr(self.model[0], "Proxy"): - PathLog.debug("hasattr() Proxy") - if isinstance(self.model[0].Proxy, ArchPanel.PanelSheet): # process the sheet - if obj.processCircles or obj.processHoles: - for shape in self.model[0].Proxy.getHoles(self.model[0], transform=True): - for wire in shape.Wires: - drillable = PathUtils.isDrillable(self.model[0].Proxy, wire) - if (drillable and obj.processCircles) or (not drillable and obj.processHoles): - f = Part.makeFace(wire, 'Part::FaceMakerSimple') - env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) - tup = env, True, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value - shapes.append(tup) - - if obj.processPerimeter: - for shape in self.model[0].Proxy.getOutlines(self.model[0], transform=True): - for wire in shape.Wires: - f = Part.makeFace(wire, 'Part::FaceMakerSimple') - env = PathUtils.getEnvelope(self.model[0].Shape, subshape=f, depthparams=self.depthparams) - tup = env, False, 'pathProfileFaces', 0.0, 'X', obj.StartDepth.Value, obj.FinalDepth.Value - shapes.append(tup) - - self.removalshapes = shapes # pylint: disable=attribute-defined-outside-init - PathLog.debug("%d shapes" % len(shapes)) - - return shapes - - def areaOpSetDefaultValues(self, obj, job): - '''areaOpSetDefaultValues(obj, job) ... sets default values for hole, circle and perimeter processing.''' - self.baseObject().areaOpSetDefaultValues(obj, job) - - obj.processHoles = False - obj.processCircles = False - obj.processPerimeter = True - obj.ReverseDirection = False - obj.InverseAngle = False - obj.AttemptInverseAngle = True - obj.LimitDepthToFace = True - obj.HandleMultipleFeatures = 'Individually' +class ObjectProfile(PathProfile.ObjectProfile): + '''Psuedo class for Profile operation, + allowing for backward compatibility with pre-existing "Profile Faces" operations.''' + pass +# Eclass def SetupProperties(): - setup = PathProfileBase.SetupProperties() - setup.append("processHoles") - setup.append("processPerimeter") - setup.append("processCircles") - setup.append("ReverseDirection") - setup.append("InverseAngle") - setup.append("AttemptInverseAngle") - setup.append("HandleMultipleFeatures") - return setup + return PathProfile.SetupProperties() def Create(name, obj=None): - '''Create(name) ... Creates and returns a Profile based on faces operation.''' + '''Create(name) ... Creates and returns a Profile operation.''' if obj is None: obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name) obj.Proxy = ObjectProfile(obj, name) diff --git a/src/Mod/Path/PathScripts/PathProfileFacesGui.py b/src/Mod/Path/PathScripts/PathProfileFacesGui.py index e56c35e0c8..b080a22eb1 100644 --- a/src/Mod/Path/PathScripts/PathProfileFacesGui.py +++ b/src/Mod/Path/PathScripts/PathProfileFacesGui.py @@ -1,53 +1,54 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * * -# * 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 PathScripts.PathOpGui as PathOpGui -import PathScripts.PathProfileBaseGui as PathProfileBaseGui -import PathScripts.PathProfileFaces as PathProfileFaces - -from PySide import QtCore - -__title__ = "Path Profile based on faces Operation UI" -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Profile based on faces operation page controller and command implementation." - -class TaskPanelOpPage(PathProfileBaseGui.TaskPanelOpPage): - '''Page controller for profile based on faces operation.''' - - def profileFeatures(self): - '''profileFeatures() ... return FeatureSide | FeatureProcessing. - See PathProfileBaseGui.py for details.''' - return PathProfileBaseGui.FeatureSide | PathProfileBaseGui.FeatureProcessing - -Command = PathOpGui.SetupOperation('Profile Faces', - PathProfileFaces.Create, - TaskPanelOpPage, - 'Path-Profile-Face', - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Face Profile"), - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile based on face or faces"), - PathProfileFaces.SetupProperties) - -FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * 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 * +# * * +# *************************************************************************** +# * Major modifications: 2020 Russell Johnson * + +import FreeCAD +import PathScripts.PathOpGui as PathOpGui +import PathScripts.PathProfile as PathProfile +import PathScripts.PathProfileGui as PathProfileGui +from PySide import QtCore + + +__title__ = "Path Profile Faces Operation UI (depreciated)" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Profile Faces operation page controller and command implementation (depreciated)." + + +class TaskPanelOpPage(PathProfileGui.TaskPanelOpPage): + '''Psuedo page controller class for Profile operation, + allowing for backward compatibility with pre-existing "Profile Faces" operations.''' + pass +# Eclass + + +Command = PathOpGui.SetupOperation('Profile', + PathProfile.Create, + TaskPanelOpPage, + 'Path-Contour', + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), + PathProfile.SetupProperties) + +FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") From e5b7a66d92b6f80421e00bdfaa4c92ec47b58e5e Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Sun, 10 May 2020 23:06:33 -0500 Subject: [PATCH 08/11] Path: Additional fixes and improvements to unified Profile operation --- src/Mod/Path/PathScripts/PathProfile.py | 123 ++++--- src/Mod/Path/PathScripts/PathProfileGui.py | 360 +++++++++++---------- 2 files changed, 265 insertions(+), 218 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathProfile.py b/src/Mod/Path/PathScripts/PathProfile.py index 6652b63a50..784632b48c 100644 --- a/src/Mod/Path/PathScripts/PathProfile.py +++ b/src/Mod/Path/PathScripts/PathProfile.py @@ -3,6 +3,7 @@ # *************************************************************************** # * * # * Copyright (c) 2014 Yorik van Havre * +# * Copyright (c) 2016 sliptonic * # * Copyright (c) 2020 Schildkroet * # * * # * This program is free software; you can redistribute it and/or modify * @@ -38,13 +39,13 @@ from PySide import QtCore from lazy_loader.lazy_loader import LazyLoader ArchPanel = LazyLoader('ArchPanel', globals(), 'ArchPanel') Part = LazyLoader('Part', globals(), 'Part') -DraftGeomUtils = LazyLoader('DraftGeomUtils', globals(), 'DraftGeomUtils') __title__ = "Path Profile Faces Operation" -__author__ = "sliptonic (Brad Collette), Schildkroet" +__author__ = "sliptonic (Brad Collette)" __url__ = "http://www.freecadweb.org" __doc__ = "Path Profile operation based on faces." +__contributors__ = "Schildkroet" PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) @@ -57,17 +58,14 @@ def translate(context, text, disambig=None): class ObjectProfile(PathAreaOp.ObjectOp): '''Proxy object for Profile operations based on faces.''' - def baseObject(self): - '''baseObject() ... returns super of receiver - Used to call base implementation in overwritten functions.''' - return super(self.__class__, self) - def areaOpFeatures(self, obj): - '''areaOpFeatures(obj) ... returns features specific to the operation''' - return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels | PathOp.FeatureBaseEdges + '''areaOpFeatures(obj) ... returns operation-specific features''' + return PathOp.FeatureBaseFaces | PathOp.FeatureBasePanels \ + | PathOp.FeatureBaseEdges def initAreaOp(self, obj): '''initAreaOp(obj) ... creates all profile specific properties.''' + self.propertiesReady = False self.initAreaOpProperties(obj) obj.setEditorMode('MiterLimit', 2) @@ -75,29 +73,26 @@ class ObjectProfile(PathAreaOp.ObjectOp): def initAreaOpProperties(self, obj, warn=False): '''initAreaOpProperties(obj) ... create operation specific properties''' - missing = list() - JOB = PathUtils.findParentJob(obj) + self.addNewProps = list() for (prtyp, nm, grp, tt) in self.areaOpProperties(): if not hasattr(obj, nm): obj.addProperty(prtyp, nm, grp, tt) - missing.append(nm) - if warn: - newPropMsg = translate('PathProfile', 'New property added to') + ' "{}": '.format(obj.Label) + nm + '. ' - newPropMsg += translate('PathProfile', 'Check its default value.') - PathLog.warning(newPropMsg) + self.addNewProps.append(nm) - if len(missing) > 0: + if len(self.addNewProps) > 0: # Set enumeration lists for enumeration properties ENUMS = self.areaOpPropertyEnumerations() for n in ENUMS: - if n in missing: + if n in self.addNewProps: setattr(obj, n, ENUMS[n]) - # Set default values - PROP_DFLTS = self.areaOpPropertyDefaults(obj, JOB) - for n in PROP_DFLTS: - if n in missing: - setattr(obj, n, PROP_DFLTS[n]) + if warn: + newPropMsg = translate('PathProfile', 'New property added to') + newPropMsg += ' "{}": {}'.format(obj.Label, self.addNewProps) + '. ' + newPropMsg += translate('PathProfile', 'Check its default value.') + '\n' + FreeCAD.Console.PrintWarning(newPropMsg) + + self.propertiesReady = True def areaOpProperties(self): '''areaOpProperties(obj) ... returns a tuples. @@ -146,8 +141,8 @@ class ObjectProfile(PathAreaOp.ObjectOp): 'Side': ['Outside', 'Inside'], # side of profile that cutter is on in relation to direction of profile } - def areaOpPropertyDefaults(self, obj=None, job=None): - '''areaOpPropertyDefaults(obj=None, job=None) ... returns a dictionary of default values + def areaOpPropertyDefaults(self, obj, job): + '''areaOpPropertyDefaults(obj, job) ... returns a dictionary of default values for the operation's properties.''' return { 'AttemptInverseAngle': True, @@ -166,40 +161,77 @@ class ObjectProfile(PathAreaOp.ObjectOp): 'processPerimeter': True } - def areaOpOnChanged(self, obj, prop): - '''areaOpOnChanged(obj, prop) ... updates certain property visibilities depending on changed properties.''' - if prop in ['UseComp', 'JoinType', 'EnableRotation']: - self.setOpEditorProperties(obj) + 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') + 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.''' - side = 2 - if obj.UseComp: - if len(obj.Base) > 0: - side = 0 + fc = 2 + # ml = 0 if obj.JoinType == 'Miter' else 2 + rotation = 2 if obj.EnableRotation == 'Off' else 0 + 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) - if obj.JoinType == 'Miter': - obj.setEditorMode('MiterLimit', 0) - else: - obj.setEditorMode('MiterLimit', 2) - - rotation = 2 - if obj.EnableRotation != 'Off': - rotation = 0 obj.setEditorMode('ReverseDirection', rotation) obj.setEditorMode('InverseAngle', rotation) obj.setEditorMode('AttemptInverseAngle', rotation) obj.setEditorMode('LimitDepthToFace', rotation) + def getOperationType(self, obj): + if len(obj.Base) == 0: + return 'Contour' + + # return first geometry type selected + (base, subsList) = obj.Base[0] + return subsList[0][:4] + def areaOpOnDocumentRestored(self, obj): + self.propertiesReady = False + self.initAreaOpProperties(obj, warn=True) - - for prop in ['UseComp', 'JoinType']: - self.areaOpOnChanged(obj, prop) - + 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', 'EnableRotation', '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.''' @@ -499,6 +531,7 @@ class ObjectProfile(PathAreaOp.ObjectOp): # Edges pre-processing def _processEdges(self, obj): + import DraftGeomUtils shapes = list() basewires = list() delPairs = list() diff --git a/src/Mod/Path/PathScripts/PathProfileGui.py b/src/Mod/Path/PathScripts/PathProfileGui.py index 756ff2bd30..3e4ea54c9a 100644 --- a/src/Mod/Path/PathScripts/PathProfileGui.py +++ b/src/Mod/Path/PathScripts/PathProfileGui.py @@ -1,173 +1,187 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * * -# * 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 FreeCADGui -import PathScripts.PathGui as PathGui -import PathScripts.PathOpGui as PathOpGui -import PathScripts.PathProfile as PathProfile - -from PySide import QtCore - - -__title__ = "Path Profile Operation UI" -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Profile operation page controller and command implementation." - - -FeatureSide = 0x01 -FeatureProcessing = 0x02 - -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -class TaskPanelOpPage(PathOpGui.TaskPanelPage): - '''Base class for profile operation page controllers. Two sub features are supported: - FeatureSide ... Is the Side property exposed in the UI - FeatureProcessing ... Are the processing check boxes supported by the operation - ''' - - def initPage(self, obj): - self.updateVisibility(obj) - - def profileFeatures(self): - '''profileFeatures() ... return which of the optional profile features are supported. - Currently two features are supported and returned: - FeatureSide ... Is the Side property exposed in the UI - FeatureProcessing ... Are the processing check boxes supported by the operation - .''' - return FeatureSide | FeatureProcessing - - def getForm(self): - '''getForm() ... returns UI customized according to profileFeatures()''' - form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui") - return form - - def getFields(self, obj): - '''getFields(obj) ... transfers values from UI to obj's proprties''' - self.updateToolController(obj, self.form.toolController) - self.updateCoolant(obj, self.form.coolantController) - - if obj.Side != str(self.form.cutSide.currentText()): - obj.Side = str(self.form.cutSide.currentText()) - if obj.Direction != str(self.form.direction.currentText()): - obj.Direction = str(self.form.direction.currentText()) - PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) - if obj.EnableRotation != str(self.form.enableRotation.currentText()): - obj.EnableRotation = str(self.form.enableRotation.currentText()) - - if obj.UseComp != self.form.useCompensation.isChecked(): - obj.UseComp = self.form.useCompensation.isChecked() - if obj.UseStartPoint != self.form.useStartPoint.isChecked(): - obj.UseStartPoint = self.form.useStartPoint.isChecked() - - if obj.processHoles != self.form.processHoles.isChecked(): - obj.processHoles = self.form.processHoles.isChecked() - if obj.processPerimeter != self.form.processPerimeter.isChecked(): - obj.processPerimeter = self.form.processPerimeter.isChecked() - if obj.processCircles != self.form.processCircles.isChecked(): - obj.processCircles = self.form.processCircles.isChecked() - - def setFields(self, obj): - '''setFields(obj) ... transfers obj's property values to UI''' - self.setupToolController(obj, self.form.toolController) - self.setupCoolant(obj, self.form.coolantController) - - self.selectInComboBox(obj.Side, self.form.cutSide) - self.selectInComboBox(obj.Direction, self.form.direction) - self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) - self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) - - self.form.useCompensation.setChecked(obj.UseComp) - self.form.useStartPoint.setChecked(obj.UseStartPoint) - self.form.processHoles.setChecked(obj.processHoles) - self.form.processPerimeter.setChecked(obj.processPerimeter) - self.form.processCircles.setChecked(obj.processCircles) - - self.updateVisibility(obj) - - def getSignalsForUpdate(self, obj): - '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' - signals = [] - signals.append(self.form.toolController.currentIndexChanged) - signals.append(self.form.coolantController.currentIndexChanged) - signals.append(self.form.cutSide.currentIndexChanged) - signals.append(self.form.direction.currentIndexChanged) - signals.append(self.form.extraOffset.editingFinished) - signals.append(self.form.enableRotation.currentIndexChanged) - signals.append(self.form.useCompensation.stateChanged) - signals.append(self.form.useStartPoint.stateChanged) - signals.append(self.form.processHoles.stateChanged) - signals.append(self.form.processPerimeter.stateChanged) - signals.append(self.form.processCircles.stateChanged) - - return signals - - def updateVisibility(self, obj): - hasFace = False - fullModel = False - if len(obj.Base) > 0: - for (base, subsList) in obj.Base: - for sub in subsList: - if sub[:4] == 'Face': - hasFace = True - break - else: - fullModel = True - - if hasFace: - self.form.processCircles.show() - self.form.processHoles.show() - self.form.processPerimeter.show() - else: - self.form.processCircles.hide() - self.form.processHoles.hide() - self.form.processPerimeter.hide() - - if self.form.useCompensation.isChecked() is True and not fullModel: - self.form.cutSide.show() - self.form.cutSideLabel.show() - else: - # Reset cutSide to 'Outside' for full model before hiding cutSide input - if self.form.cutSide.currentText() == 'Inside': - self.selectInComboBox('Outside', self.form.cutSide) - self.form.cutSide.hide() - self.form.cutSideLabel.hide() - - def registerSignalHandlers(self, obj): - self.form.useCompensation.stateChanged.connect(self.updateVisibility) -# Eclass - - -Command = PathOpGui.SetupOperation('Profile', - PathProfile.Create, - TaskPanelOpPage, - 'Path-Contour', - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), - QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), - PathProfile.SetupProperties) - -FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * 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 FreeCADGui +import PathScripts.PathGui as PathGui +import PathScripts.PathOpGui as PathOpGui +import PathScripts.PathProfile as PathProfile + +from PySide import QtCore + + +__title__ = "Path Profile Operation UI" +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Profile operation page controller and command implementation." + + +FeatureSide = 0x01 +FeatureProcessing = 0x02 + +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +class TaskPanelOpPage(PathOpGui.TaskPanelPage): + '''Base class for profile operation page controllers. Two sub features are supported: + FeatureSide ... Is the Side property exposed in the UI + FeatureProcessing ... Are the processing check boxes supported by the operation + ''' + + def initPage(self, obj): + self.updateVisibility(obj) + + def profileFeatures(self): + '''profileFeatures() ... return which of the optional profile features are supported. + Currently two features are supported and returned: + FeatureSide ... Is the Side property exposed in the UI + FeatureProcessing ... Are the processing check boxes supported by the operation + .''' + return FeatureSide | FeatureProcessing + + def getForm(self): + '''getForm() ... returns UI customized according to profileFeatures()''' + form = FreeCADGui.PySideUic.loadUi(":/panels/PageOpProfileFullEdit.ui") + return form + + def getFields(self, obj): + '''getFields(obj) ... transfers values from UI to obj's proprties''' + self.updateToolController(obj, self.form.toolController) + self.updateCoolant(obj, self.form.coolantController) + + if obj.Side != str(self.form.cutSide.currentText()): + obj.Side = str(self.form.cutSide.currentText()) + if obj.Direction != str(self.form.direction.currentText()): + obj.Direction = str(self.form.direction.currentText()) + PathGui.updateInputField(obj, 'OffsetExtra', self.form.extraOffset) + if obj.EnableRotation != str(self.form.enableRotation.currentText()): + obj.EnableRotation = str(self.form.enableRotation.currentText()) + + if obj.UseComp != self.form.useCompensation.isChecked(): + obj.UseComp = self.form.useCompensation.isChecked() + if obj.UseStartPoint != self.form.useStartPoint.isChecked(): + obj.UseStartPoint = self.form.useStartPoint.isChecked() + + if obj.processHoles != self.form.processHoles.isChecked(): + obj.processHoles = self.form.processHoles.isChecked() + if obj.processPerimeter != self.form.processPerimeter.isChecked(): + obj.processPerimeter = self.form.processPerimeter.isChecked() + if obj.processCircles != self.form.processCircles.isChecked(): + obj.processCircles = self.form.processCircles.isChecked() + + def setFields(self, obj): + '''setFields(obj) ... transfers obj's property values to UI''' + self.setupToolController(obj, self.form.toolController) + self.setupCoolant(obj, self.form.coolantController) + + self.selectInComboBox(obj.Side, self.form.cutSide) + self.selectInComboBox(obj.Direction, self.form.direction) + self.form.extraOffset.setText(FreeCAD.Units.Quantity(obj.OffsetExtra.Value, FreeCAD.Units.Length).UserString) + self.selectInComboBox(obj.EnableRotation, self.form.enableRotation) + + self.form.useCompensation.setChecked(obj.UseComp) + self.form.useStartPoint.setChecked(obj.UseStartPoint) + self.form.processHoles.setChecked(obj.processHoles) + self.form.processPerimeter.setChecked(obj.processPerimeter) + self.form.processCircles.setChecked(obj.processCircles) + + self.updateVisibility(obj) + + def getSignalsForUpdate(self, obj): + '''getSignalsForUpdate(obj) ... return list of signals for updating obj''' + signals = [] + signals.append(self.form.toolController.currentIndexChanged) + signals.append(self.form.coolantController.currentIndexChanged) + signals.append(self.form.cutSide.currentIndexChanged) + signals.append(self.form.direction.currentIndexChanged) + signals.append(self.form.extraOffset.editingFinished) + signals.append(self.form.enableRotation.currentIndexChanged) + signals.append(self.form.useCompensation.stateChanged) + signals.append(self.form.useStartPoint.stateChanged) + signals.append(self.form.processHoles.stateChanged) + signals.append(self.form.processPerimeter.stateChanged) + signals.append(self.form.processCircles.stateChanged) + + return signals + + def updateVisibility(self, sentObj=None): + hasFace = False + hasGeom = False + fullModel = False + objBase = list() + + if sentObj: + if hasattr(sentObj, 'Base'): + objBase = sentObj.Base + elif hasattr(self.obj, 'Base'): + objBase = self.obj.Base + + if objBase.__len__() > 0: + for (base, subsList) in objBase: + for sub in subsList: + if sub[:4] == 'Face': + hasFace = True + break + else: + fullModel = True + + if hasFace: + self.form.processCircles.show() + self.form.processHoles.show() + self.form.processPerimeter.show() + else: + self.form.processCircles.hide() + self.form.processHoles.hide() + self.form.processPerimeter.hide() + + side = False + if self.form.useCompensation.isChecked() is True: + if not fullModel: + side = True + + if side: + self.form.cutSide.show() + self.form.cutSideLabel.show() + else: + # Reset cutSide to 'Outside' for full model before hiding cutSide input + if self.form.cutSide.currentText() == 'Inside': + self.selectInComboBox('Outside', self.form.cutSide) + self.form.cutSide.hide() + self.form.cutSideLabel.hide() + + def registerSignalHandlers(self, obj): + self.form.useCompensation.stateChanged.connect(self.updateVisibility) +# Eclass + + +Command = PathOpGui.SetupOperation('Profile', + PathProfile.Create, + TaskPanelOpPage, + 'Path-Contour', + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile"), + QtCore.QT_TRANSLATE_NOOP("PathProfile", "Profile entire model, selected face(s) or selected edge(s)"), + PathProfile.SetupProperties) + +FreeCAD.Console.PrintLog("Loading PathProfileFacesGui... done\n") From ad03af3e4391a73ab80b743fdf98b61edb24c41e Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 7 May 2020 23:16:50 -0500 Subject: [PATCH 09/11] Path: PEP8 and LGTM cleanup; Remove extra `addProperty()` statement Removed 'EnableRotation' property addition because it is done in PathAreaOp module upon creation and document restore. --- src/Mod/Path/PathScripts/PathOp.py | 1170 ++++++++++++++-------------- 1 file changed, 584 insertions(+), 586 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathOp.py b/src/Mod/Path/PathScripts/PathOp.py index a4d3a7807f..f810ba0139 100644 --- a/src/Mod/Path/PathScripts/PathOp.py +++ b/src/Mod/Path/PathScripts/PathOp.py @@ -1,586 +1,584 @@ -# -*- coding: utf-8 -*- - -# *************************************************************************** -# * * -# * Copyright (c) 2017 sliptonic * -# * * -# * 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 PathScripts.PathGeom as PathGeom -import PathScripts.PathLog as PathLog -import PathScripts.PathUtil as PathUtil -import PathScripts.PathUtils as PathUtils - -from PathScripts.PathUtils import waiting_effects -from PySide import QtCore -import time - -# lazily loaded modules -from lazy_loader.lazy_loader import LazyLoader -Part = LazyLoader('Part', globals(), 'Part') - -__title__ = "Base class for all operations." -__author__ = "sliptonic (Brad Collette)" -__url__ = "http://www.freecadweb.org" -__doc__ = "Base class and properties implementation for all Path operations." - -PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) - - -# Qt translation handling -def translate(context, text, disambig=None): - return QtCore.QCoreApplication.translate(context, text, disambig) - - -FeatureTool = 0x0001 # ToolController -FeatureDepths = 0x0002 # FinalDepth, StartDepth -FeatureHeights = 0x0004 # ClearanceHeight, SafeHeight -FeatureStartPoint = 0x0008 # StartPoint -FeatureFinishDepth = 0x0010 # FinishDepth -FeatureStepDown = 0x0020 # StepDown -FeatureNoFinalDepth = 0x0040 # edit or not edit FinalDepth -FeatureBaseVertexes = 0x0100 # Base -FeatureBaseEdges = 0x0200 # Base -FeatureBaseFaces = 0x0400 # Base -FeatureBasePanels = 0x0800 # Base -FeatureLocations = 0x1000 # Locations -FeatureCoolant = 0x2000 # Coolant - -FeatureBaseGeometry = FeatureBaseVertexes | FeatureBaseFaces | FeatureBaseEdges | FeatureBasePanels | FeatureCoolant - - -class ObjectOp(object): - ''' - Base class for proxy objects of all Path operations. - - Use this class as a base class for new operations. It provides properties - and some functionality for the standard properties each operation supports. - By OR'ing features from the feature list an operation can select which ones - of the standard features it requires and/or supports. - - The currently supported features are: - FeatureTool ... Use of a ToolController - FeatureDepths ... Depths, for start, final - FeatureHeights ... Heights, safe and clearance - FeatureStartPoint ... Supports setting a start point - FeatureFinishDepth ... Operation supports a finish depth - FeatureStepDown ... Support for step down - FeatureNoFinalDepth ... Disable support for final depth modifications - FeatureBaseVertexes ... Base geometry support for vertexes - FeatureBaseEdges ... Base geometry support for edges - FeatureBaseFaces ... Base geometry support for faces - FeatureBasePanels ... Base geometry support for Arch.Panels - FeatureLocations ... Base location support - FeatureCoolant ... Support for operation coolant - - The base class handles all base API and forwards calls to subclasses with - an op prefix. For instance, an op is not expected to overwrite onChanged(), - but implement the function opOnChanged(). - If a base class overwrites a base API function it should call the super's - implementation - otherwise the base functionality might be broken. - ''' - - def addBaseProperty(self, obj): - obj.addProperty("App::PropertyLinkSubListGlobal", "Base", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "The base geometry for this operation")) - - def addOpValues(self, obj, values): - if 'start' in values: - obj.addProperty("App::PropertyDistance", "OpStartDepth", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the calculated value for the StartDepth")) - obj.setEditorMode('OpStartDepth', 1) # read-only - if 'final' in values: - obj.addProperty("App::PropertyDistance", "OpFinalDepth", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the calculated value for the FinalDepth")) - obj.setEditorMode('OpFinalDepth', 1) # read-only - if 'tooldia' in values: - obj.addProperty("App::PropertyDistance", "OpToolDiameter", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the diameter of the tool")) - obj.setEditorMode('OpToolDiameter', 1) # read-only - if 'stockz' in values: - obj.addProperty("App::PropertyDistance", "OpStockZMax", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the max Z value of Stock")) - obj.setEditorMode('OpStockZMax', 1) # read-only - obj.addProperty("App::PropertyDistance", "OpStockZMin", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the min Z value of Stock")) - obj.setEditorMode('OpStockZMin', 1) # read-only - - def __init__(self, obj, name): - PathLog.track() - - obj.addProperty("App::PropertyBool", "Active", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make False, to prevent operation from generating code")) - obj.addProperty("App::PropertyString", "Comment", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "An optional comment for this Operation")) - obj.addProperty("App::PropertyString", "UserLabel", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "User Assigned Label")) - obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation")) - obj.setEditorMode('CycleTime', 1) # read-only - - features = self.opFeatures(obj) - - if FeatureBaseGeometry & features: - self.addBaseProperty(obj) - - if FeatureLocations & features: - obj.addProperty("App::PropertyVectorList", "Locations", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Base locations for this operation")) - - if FeatureTool & features: - obj.addProperty("App::PropertyLink", "ToolController", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "The tool controller that will be used to calculate the path")) - self.addOpValues(obj, ['tooldia']) - - if FeatureCoolant & features: - obj.addProperty("App::PropertyString", "CoolantMode", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Coolant mode for this operation")) - - if FeatureDepths & features: - obj.addProperty("App::PropertyDistance", "StartDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Starting Depth of Tool- first cut depth in Z")) - obj.addProperty("App::PropertyDistance", "FinalDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Final Depth of Tool- lowest value in Z")) - if FeatureNoFinalDepth & features: - obj.setEditorMode('FinalDepth', 2) # hide - self.addOpValues(obj, ['start', 'final']) - else: - # StartDepth has become necessary for expressions on other properties - obj.addProperty("App::PropertyDistance", "StartDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Starting Depth internal use only for derived values")) - obj.setEditorMode('StartDepth', 1) # read-only - - self.addOpValues(obj, ['stockz']) - - if FeatureStepDown & features: - obj.addProperty("App::PropertyDistance", "StepDown", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Incremental Step Down of Tool")) - - if FeatureFinishDepth & features: - obj.addProperty("App::PropertyDistance", "FinishDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Maximum material removed on final pass.")) - - if FeatureHeights & features: - obj.addProperty("App::PropertyDistance", "ClearanceHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "The height needed to clear clamps and obstructions")) - obj.addProperty("App::PropertyDistance", "SafeHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Rapid Safety Height between locations.")) - - if FeatureStartPoint & features: - obj.addProperty("App::PropertyVectorDistance", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path")) - obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make True, if specifying a Start Point")) - - # members being set later - self.commandlist = None - self.horizFeed = None - self.horizRapid = None - self.job = None - self.model = None - self.radius = None - self.stock = None - self.tool = None - self.vertFeed = None - self.vertRapid = None - - self.initOperation(obj) - - if not hasattr(obj, 'DoNotSetDefaultValues') or not obj.DoNotSetDefaultValues: - job = self.setDefaultValues(obj) - if job: - job.SetupSheet.Proxy.setOperationProperties(obj, name) - obj.recompute() - obj.Proxy = self - - def setEditorModes(self, obj, features): - '''Editor modes are not preserved during document store/restore, set editor modes for all properties''' - - for op in ['OpStartDepth', 'OpFinalDepth', 'OpToolDiameter', 'CycleTime']: - if hasattr(obj, op): - obj.setEditorMode(op, 1) # read-only - - if FeatureDepths & features: - if FeatureNoFinalDepth & features: - obj.setEditorMode('OpFinalDepth', 2) - - def onDocumentRestored(self, obj): - features = self.opFeatures(obj) - if FeatureBaseGeometry & features and 'App::PropertyLinkSubList' == obj.getTypeIdOfProperty('Base'): - PathLog.info("Replacing link property with global link (%s)." % obj.State) - base = obj.Base - obj.removeProperty('Base') - self.addBaseProperty(obj) - obj.Base = base - obj.touch() - obj.Document.recompute() - - if FeatureTool & features and not hasattr(obj, 'OpToolDiameter'): - self.addOpValues(obj, ['tooldia']) - - if FeatureCoolant & features and not hasattr(obj, 'CoolantMode'): - obj.addProperty("App::PropertyString", "CoolantMode", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Coolant option for this operation")) - - if FeatureDepths & features and not hasattr(obj, 'OpStartDepth'): - self.addOpValues(obj, ['start', 'final']) - if FeatureNoFinalDepth & features: - obj.setEditorMode('OpFinalDepth', 2) - - if not hasattr(obj, 'OpStockZMax'): - self.addOpValues(obj, ['stockz']) - - if not hasattr(obj, 'EnableRotation'): - obj.addProperty("App::PropertyEnumeration", "EnableRotation", "Rotation", QtCore.QT_TRANSLATE_NOOP("App::Property", "Enable rotation to gain access to pockets/areas not normal to Z axis.")) - obj.EnableRotation = ['Off', 'A(x)', 'B(y)', 'A & B'] - - if not hasattr(obj, 'CycleTime'): - obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation")) - - self.setEditorModes(obj, features) - self.opOnDocumentRestored(obj) - - def __getstate__(self): - '''__getstat__(self) ... called when receiver is saved. - Can safely be overwritten by subclasses.''' - return None - - def __setstate__(self, state): - '''__getstat__(self) ... called when receiver is restored. - Can safely be overwritten by subclasses.''' - return None - - def opFeatures(self, obj): - '''opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation. - The default implementation returns "FeatureTool | FeatureDeptsh | FeatureHeights | FeatureStartPoint" - Should be overwritten by subclasses.''' - # pylint: disable=unused-argument - return FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint | FeatureBaseGeometry | FeatureFinishDepth | FeatureCoolant - - def initOperation(self, obj): - '''initOperation(obj) ... implement to create additional properties. - Should be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def opOnDocumentRestored(self, obj): - '''opOnDocumentRestored(obj) ... implement if an op needs special handling like migrating the data model. - Should be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def opOnChanged(self, obj, prop): - '''opOnChanged(obj, prop) ... overwrite to process property changes. - This is a callback function that is invoked each time a property of the - receiver is assigned a value. Note that the FC framework does not - distinguish between assigning a different value and assigning the same - value again. - Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def opSetDefaultValues(self, obj, job): - '''opSetDefaultValues(obj, job) ... overwrite to set initial default values. - Called after the receiver has been fully created with all properties. - Can safely be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def opUpdateDepths(self, obj): - '''opUpdateDepths(obj) ... overwrite to implement special depths calculation. - Can safely be overwritten by subclass.''' - pass # pylint: disable=unnecessary-pass - - def opExecute(self, obj): - '''opExecute(obj) ... called whenever the receiver needs to be recalculated. - See documentation of execute() for a list of base functionality provided. - Should be overwritten by subclasses.''' - pass # pylint: disable=unnecessary-pass - - def opRejectAddBase(self, obj, base, sub): - '''opRejectAddBase(base, sub) ... if op returns True the addition of the feature is prevented. - Should be overwritten by subclasses.''' - # pylint: disable=unused-argument - return False - - def onChanged(self, obj, prop): - '''onChanged(obj, prop) ... base implementation of the FC notification framework. - Do not overwrite, overwrite opOnChanged() instead.''' - if 'Restore' not in obj.State and prop in ['Base', 'StartDepth', 'FinalDepth']: - self.updateDepths(obj, True) - - self.opOnChanged(obj, prop) - - def applyExpression(self, obj, prop, expr): - '''applyExpression(obj, prop, expr) ... set expression expr on obj.prop if expr is set''' - if expr: - obj.setExpression(prop, expr) - return True - return False - - def setDefaultValues(self, obj): - '''setDefaultValues(obj) ... base implementation. - Do not overwrite, overwrite opSetDefaultValues() instead.''' - job = PathUtils.addToJob(obj) - - obj.Active = True - - features = self.opFeatures(obj) - - if FeatureTool & features: - if 1 < len(job.Operations.Group): - obj.ToolController = PathUtil.toolControllerForOp(job.Operations.Group[-2]) - else: - obj.ToolController = PathUtils.findToolController(obj) - if not obj.ToolController: - return None - obj.OpToolDiameter = obj.ToolController.Tool.Diameter - - if FeatureCoolant & features: - obj.CoolantMode = job.SetupSheet.CoolantMode - - if FeatureDepths & features: - if self.applyExpression(obj, 'StartDepth', job.SetupSheet.StartDepthExpression): - obj.OpStartDepth = 1.0 - else: - obj.StartDepth = 1.0 - if self.applyExpression(obj, 'FinalDepth', job.SetupSheet.FinalDepthExpression): - obj.OpFinalDepth = 0.0 - else: - obj.FinalDepth = 0.0 - else: - obj.StartDepth = 1.0 - - if FeatureStepDown & features: - if not self.applyExpression(obj, 'StepDown', job.SetupSheet.StepDownExpression): - obj.StepDown = '1 mm' - - if FeatureHeights & features: - if job.SetupSheet.SafeHeightExpression: - if not self.applyExpression(obj, 'SafeHeight', job.SetupSheet.SafeHeightExpression): - obj.SafeHeight = '3 mm' - if job.SetupSheet.ClearanceHeightExpression: - if not self.applyExpression(obj, 'ClearanceHeight', job.SetupSheet.ClearanceHeightExpression): - obj.ClearanceHeight = '5 mm' - - if FeatureStartPoint & features: - obj.UseStartPoint = False - - self.opSetDefaultValues(obj, job) - return job - - def _setBaseAndStock(self, obj, ignoreErrors=False): - job = PathUtils.findParentJob(obj) - if not job: - if not ignoreErrors: - PathLog.error(translate("Path", "No parent job found for operation.")) - return False - if not job.Model.Group: - if not ignoreErrors: - PathLog.error(translate("Path", "Parent job %s doesn't have a base object") % job.Label) - return False - self.job = job - self.model = job.Model.Group - self.stock = job.Stock - return True - - def getJob(self, obj): - '''getJob(obj) ... return the job this operation is part of.''' - if not hasattr(self, 'job') or self.job is None: - if not self._setBaseAndStock(obj): - return None - return self.job - - def updateDepths(self, obj, ignoreErrors=False): - '''updateDepths(obj) ... base implementation calculating depths depending on base geometry. - Should not be overwritten.''' - - def faceZmin(bb, fbb): - if fbb.ZMax == fbb.ZMin and fbb.ZMax == bb.ZMax: # top face - return fbb.ZMin - elif fbb.ZMax > fbb.ZMin and fbb.ZMax == bb.ZMax: # vertical face, full cut - return fbb.ZMin - elif fbb.ZMax > fbb.ZMin and fbb.ZMin > bb.ZMin: # internal vertical wall - return fbb.ZMin - elif fbb.ZMax == fbb.ZMin and fbb.ZMax > bb.ZMin: # face/shelf - return fbb.ZMin - return bb.ZMin - - if not self._setBaseAndStock(obj, ignoreErrors): - return False - - stockBB = self.stock.Shape.BoundBox - zmin = stockBB.ZMin - zmax = stockBB.ZMax - - obj.OpStockZMin = zmin - obj.OpStockZMax = zmax - - if hasattr(obj, 'Base') and obj.Base: - for base, sublist in obj.Base: - bb = base.Shape.BoundBox - zmax = max(zmax, bb.ZMax) - for sub in sublist: - try: - fbb = base.Shape.getElement(sub).BoundBox - zmin = max(zmin, faceZmin(bb, fbb)) - zmax = max(zmax, fbb.ZMax) - except Part.OCCError as e: - PathLog.error(e) - - else: - # clearing with stock boundaries - job = PathUtils.findParentJob(obj) - zmax = stockBB.ZMax - zmin = job.Proxy.modelBoundBox(job).ZMax - - if FeatureDepths & self.opFeatures(obj): - # first set update final depth, it's value is not negotiable - if not PathGeom.isRoughly(obj.OpFinalDepth.Value, zmin): - obj.OpFinalDepth = zmin - zmin = obj.OpFinalDepth.Value - - def minZmax(z): - if hasattr(obj, 'StepDown') and not PathGeom.isRoughly(obj.StepDown.Value, 0): - return z + obj.StepDown.Value - else: - return z + 1 - - # ensure zmax is higher than zmin - if (zmax - 0.0001) <= zmin: - zmax = minZmax(zmin) - - # update start depth if requested and required - if not PathGeom.isRoughly(obj.OpStartDepth.Value, zmax): - obj.OpStartDepth = zmax - else: - # every obj has a StartDepth - if obj.StartDepth.Value != zmax: - obj.StartDepth = zmax - - self.opUpdateDepths(obj) - - @waiting_effects - def execute(self, obj): - '''execute(obj) ... base implementation - do not overwrite! - Verifies that the operation is assigned to a job and that the job also has a valid Base. - It also sets the following instance variables that can and should be safely be used by - implementation of opExecute(): - self.model ... List of base objects of the Job itself - self.stock ... Stock object for the Job itself - self.vertFeed ... vertical feed rate of assigned tool - self.vertRapid ... vertical rapid rate of assigned tool - self.horizFeed ... horizontal feed rate of assigned tool - self.horizRapid ... norizontal rapid rate of assigned tool - self.tool ... the actual tool being used - self.radius ... the main radius of the tool being used - self.commandlist ... a list for collecting all commands produced by the operation - - Once everything is validated and above variables are set the implementation calls - opExecute(obj) - which is expected to add the generated commands to self.commandlist - Finally the base implementation adds a rapid move to clearance height and assigns - the receiver's Path property from the command list. - ''' - PathLog.track() - - if obj.ViewObject: - obj.ViewObject.Visibility = obj.Active - - if not obj.Active: - path = Path.Path("(inactive operation)") - obj.Path = path - return - - if not self._setBaseAndStock(obj): - return - - if FeatureCoolant & self.opFeatures(obj): - if not hasattr(obj, 'CoolantMode'): - PathLog.error(translate("Path", "No coolant property found. Please recreate operation.")) - - if FeatureTool & self.opFeatures(obj): - tc = obj.ToolController - if tc is None or tc.ToolNumber == 0: - PathLog.error(translate("Path", "No Tool Controller is selected. We need a tool to build a Path.")) - return - else: - self.vertFeed = tc.VertFeed.Value - self.horizFeed = tc.HorizFeed.Value - self.vertRapid = tc.VertRapid.Value - self.horizRapid = tc.HorizRapid.Value - tool = tc.Proxy.getTool(tc) - if not tool or float(tool.Diameter) == 0: - PathLog.error(translate("Path", "No Tool found or diameter is zero. We need a tool to build a Path.")) - return - self.radius = float(tool.Diameter) / 2 - self.tool = tool - obj.OpToolDiameter = tool.Diameter - - self.updateDepths(obj) - # now that all op values are set make sure the user properties get updated accordingly, - # in case they still have an expression referencing any op values - obj.recompute() - - self.commandlist = [] - self.commandlist.append(Path.Command("(%s)" % obj.Label)) - if obj.Comment: - self.commandlist.append(Path.Command("(%s)" % obj.Comment)) - - result = self.opExecute(obj) # pylint: disable=assignment-from-no-return - - if FeatureHeights & self.opFeatures(obj): - # Let's finish by rapid to clearance...just for safety - self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) - - path = Path.Path(self.commandlist) - obj.Path = path - obj.CycleTime = self.getCycleTimeEstimate(obj) - self.job.Proxy.getCycleTime() - return result - - def getCycleTimeEstimate(self, obj): - - tc = obj.ToolController - - if tc is None or tc.ToolNumber == 0: - PathLog.error(translate("Path", "No Tool Controller selected.")) - return translate('Path', 'Tool Error') - - hFeedrate = tc.HorizFeed.Value - vFeedrate = tc.VertFeed.Value - hRapidrate = tc.HorizRapid.Value - vRapidrate = tc.VertRapid.Value - - if hFeedrate == 0 or vFeedrate == 0: - PathLog.warning(translate("Path", "Tool Controller feedrates required to calculate the cycle time.")) - return translate('Path', 'Feedrate Error') - - if hRapidrate == 0 or vRapidrate == 0: - PathLog.warning(translate("Path", "Add Tool Controller Rapid Speeds on the SetupSheet for more accurate cycle times.")) - - # Get the cycle time in seconds - seconds = obj.Path.getCycleTime(hFeedrate, vFeedrate, hRapidrate, vRapidrate) - - if not seconds: - return translate('Path', 'Cycletime Error') - - # Convert the cycle time to a HH:MM:SS format - cycleTime = time.strftime("%H:%M:%S", time.gmtime(seconds)) - - return cycleTime - - def addBase(self, obj, base, sub): - PathLog.track(obj, base, sub) - base = PathUtil.getPublicObject(base) - - if self._setBaseAndStock(obj): - for model in self.job.Model.Group: - if base == self.job.Proxy.baseObject(self.job, model): - base = model - break - - baselist = obj.Base - if baselist is None: - baselist = [] - - for p, el in baselist: - if p == base and sub in el: - PathLog.notice((translate("Path", "Base object %s.%s already in the list") + "\n") % (base.Label, sub)) - return - - if not self.opRejectAddBase(obj, base, sub): - baselist.append((base, sub)) - obj.Base = baselist - else: - PathLog.notice((translate("Path", "Base object %s.%s rejected by operation") + "\n") % (base.Label, sub)) +# -*- coding: utf-8 -*- + +# *************************************************************************** +# * * +# * Copyright (c) 2017 sliptonic * +# * * +# * 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 PathScripts.PathGeom as PathGeom +import PathScripts.PathLog as PathLog +import PathScripts.PathUtil as PathUtil +import PathScripts.PathUtils as PathUtils + +from PathScripts.PathUtils import waiting_effects +from PySide import QtCore +import time + +# lazily loaded modules +from lazy_loader.lazy_loader import LazyLoader +Part = LazyLoader('Part', globals(), 'Part') + +__title__ = "Base class for all operations." +__author__ = "sliptonic (Brad Collette)" +__url__ = "http://www.freecadweb.org" +__doc__ = "Base class and properties implementation for all Path operations." + +PathLog.setLevel(PathLog.Level.INFO, PathLog.thisModule()) +# PathLog.trackModule() + + +# Qt translation handling +def translate(context, text, disambig=None): + return QtCore.QCoreApplication.translate(context, text, disambig) + + +FeatureTool = 0x0001 # ToolController +FeatureDepths = 0x0002 # FinalDepth, StartDepth +FeatureHeights = 0x0004 # ClearanceHeight, SafeHeight +FeatureStartPoint = 0x0008 # StartPoint +FeatureFinishDepth = 0x0010 # FinishDepth +FeatureStepDown = 0x0020 # StepDown +FeatureNoFinalDepth = 0x0040 # edit or not edit FinalDepth +FeatureBaseVertexes = 0x0100 # Base +FeatureBaseEdges = 0x0200 # Base +FeatureBaseFaces = 0x0400 # Base +FeatureBasePanels = 0x0800 # Base +FeatureLocations = 0x1000 # Locations +FeatureCoolant = 0x2000 # Coolant + +FeatureBaseGeometry = FeatureBaseVertexes | FeatureBaseFaces | FeatureBaseEdges | FeatureBasePanels | FeatureCoolant + + +class ObjectOp(object): + ''' + Base class for proxy objects of all Path operations. + + Use this class as a base class for new operations. It provides properties + and some functionality for the standard properties each operation supports. + By OR'ing features from the feature list an operation can select which ones + of the standard features it requires and/or supports. + + The currently supported features are: + FeatureTool ... Use of a ToolController + FeatureDepths ... Depths, for start, final + FeatureHeights ... Heights, safe and clearance + FeatureStartPoint ... Supports setting a start point + FeatureFinishDepth ... Operation supports a finish depth + FeatureStepDown ... Support for step down + FeatureNoFinalDepth ... Disable support for final depth modifications + FeatureBaseVertexes ... Base geometry support for vertexes + FeatureBaseEdges ... Base geometry support for edges + FeatureBaseFaces ... Base geometry support for faces + FeatureBasePanels ... Base geometry support for Arch.Panels + FeatureLocations ... Base location support + FeatureCoolant ... Support for operation coolant + + The base class handles all base API and forwards calls to subclasses with + an op prefix. For instance, an op is not expected to overwrite onChanged(), + but implement the function opOnChanged(). + If a base class overwrites a base API function it should call the super's + implementation - otherwise the base functionality might be broken. + ''' + + def addBaseProperty(self, obj): + obj.addProperty("App::PropertyLinkSubListGlobal", "Base", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "The base geometry for this operation")) + + def addOpValues(self, obj, values): + if 'start' in values: + obj.addProperty("App::PropertyDistance", "OpStartDepth", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the calculated value for the StartDepth")) + obj.setEditorMode('OpStartDepth', 1) # read-only + if 'final' in values: + obj.addProperty("App::PropertyDistance", "OpFinalDepth", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the calculated value for the FinalDepth")) + obj.setEditorMode('OpFinalDepth', 1) # read-only + if 'tooldia' in values: + obj.addProperty("App::PropertyDistance", "OpToolDiameter", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the diameter of the tool")) + obj.setEditorMode('OpToolDiameter', 1) # read-only + if 'stockz' in values: + obj.addProperty("App::PropertyDistance", "OpStockZMax", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the max Z value of Stock")) + obj.setEditorMode('OpStockZMax', 1) # read-only + obj.addProperty("App::PropertyDistance", "OpStockZMin", "Op Values", QtCore.QT_TRANSLATE_NOOP("PathOp", "Holds the min Z value of Stock")) + obj.setEditorMode('OpStockZMin', 1) # read-only + + def __init__(self, obj, name): + PathLog.track() + + obj.addProperty("App::PropertyBool", "Active", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make False, to prevent operation from generating code")) + obj.addProperty("App::PropertyString", "Comment", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "An optional comment for this Operation")) + obj.addProperty("App::PropertyString", "UserLabel", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "User Assigned Label")) + obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation")) + obj.setEditorMode('CycleTime', 1) # read-only + + features = self.opFeatures(obj) + + if FeatureBaseGeometry & features: + self.addBaseProperty(obj) + + if FeatureLocations & features: + obj.addProperty("App::PropertyVectorList", "Locations", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Base locations for this operation")) + + if FeatureTool & features: + obj.addProperty("App::PropertyLink", "ToolController", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "The tool controller that will be used to calculate the path")) + self.addOpValues(obj, ['tooldia']) + + if FeatureCoolant & features: + obj.addProperty("App::PropertyString", "CoolantMode", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Coolant mode for this operation")) + + if FeatureDepths & features: + obj.addProperty("App::PropertyDistance", "StartDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Starting Depth of Tool- first cut depth in Z")) + obj.addProperty("App::PropertyDistance", "FinalDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Final Depth of Tool- lowest value in Z")) + if FeatureNoFinalDepth & features: + obj.setEditorMode('FinalDepth', 2) # hide + self.addOpValues(obj, ['start', 'final']) + else: + # StartDepth has become necessary for expressions on other properties + obj.addProperty("App::PropertyDistance", "StartDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Starting Depth internal use only for derived values")) + obj.setEditorMode('StartDepth', 1) # read-only + + self.addOpValues(obj, ['stockz']) + + if FeatureStepDown & features: + obj.addProperty("App::PropertyDistance", "StepDown", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Incremental Step Down of Tool")) + + if FeatureFinishDepth & features: + obj.addProperty("App::PropertyDistance", "FinishDepth", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Maximum material removed on final pass.")) + + if FeatureHeights & features: + obj.addProperty("App::PropertyDistance", "ClearanceHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "The height needed to clear clamps and obstructions")) + obj.addProperty("App::PropertyDistance", "SafeHeight", "Depth", QtCore.QT_TRANSLATE_NOOP("PathOp", "Rapid Safety Height between locations.")) + + if FeatureStartPoint & features: + obj.addProperty("App::PropertyVectorDistance", "StartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "The start point of this path")) + obj.addProperty("App::PropertyBool", "UseStartPoint", "Start Point", QtCore.QT_TRANSLATE_NOOP("PathOp", "Make True, if specifying a Start Point")) + + # members being set later + self.commandlist = None + self.horizFeed = None + self.horizRapid = None + self.job = None + self.model = None + self.radius = None + self.stock = None + self.tool = None + self.vertFeed = None + self.vertRapid = None + self.addNewProps = None + + self.initOperation(obj) + + if not hasattr(obj, 'DoNotSetDefaultValues') or not obj.DoNotSetDefaultValues: + job = self.setDefaultValues(obj) + if job: + job.SetupSheet.Proxy.setOperationProperties(obj, name) + obj.recompute() + obj.Proxy = self + + def setEditorModes(self, obj, features): + '''Editor modes are not preserved during document store/restore, set editor modes for all properties''' + + for op in ['OpStartDepth', 'OpFinalDepth', 'OpToolDiameter', 'CycleTime']: + if hasattr(obj, op): + obj.setEditorMode(op, 1) # read-only + + if FeatureDepths & features: + if FeatureNoFinalDepth & features: + obj.setEditorMode('OpFinalDepth', 2) + + def onDocumentRestored(self, obj): + features = self.opFeatures(obj) + if FeatureBaseGeometry & features and 'App::PropertyLinkSubList' == obj.getTypeIdOfProperty('Base'): + PathLog.info("Replacing link property with global link (%s)." % obj.State) + base = obj.Base + obj.removeProperty('Base') + self.addBaseProperty(obj) + obj.Base = base + obj.touch() + obj.Document.recompute() + + if FeatureTool & features and not hasattr(obj, 'OpToolDiameter'): + self.addOpValues(obj, ['tooldia']) + + if FeatureCoolant & features and not hasattr(obj, 'CoolantMode'): + obj.addProperty("App::PropertyString", "CoolantMode", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Coolant option for this operation")) + + if FeatureDepths & features and not hasattr(obj, 'OpStartDepth'): + self.addOpValues(obj, ['start', 'final']) + if FeatureNoFinalDepth & features: + obj.setEditorMode('OpFinalDepth', 2) + + if not hasattr(obj, 'OpStockZMax'): + self.addOpValues(obj, ['stockz']) + + if not hasattr(obj, 'CycleTime'): + obj.addProperty("App::PropertyString", "CycleTime", "Path", QtCore.QT_TRANSLATE_NOOP("PathOp", "Operations Cycle Time Estimation")) + + self.setEditorModes(obj, features) + self.opOnDocumentRestored(obj) + + def __getstate__(self): + '''__getstat__(self) ... called when receiver is saved. + Can safely be overwritten by subclasses.''' + return None + + def __setstate__(self, state): + '''__getstat__(self) ... called when receiver is restored. + Can safely be overwritten by subclasses.''' + return None + + def opFeatures(self, obj): + '''opFeatures(obj) ... returns the OR'ed list of features used and supported by the operation. + The default implementation returns "FeatureTool | FeatureDeptsh | FeatureHeights | FeatureStartPoint" + Should be overwritten by subclasses.''' + # pylint: disable=unused-argument + return FeatureTool | FeatureDepths | FeatureHeights | FeatureStartPoint | FeatureBaseGeometry | FeatureFinishDepth | FeatureCoolant + + def initOperation(self, obj): + '''initOperation(obj) ... implement to create additional properties. + Should be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def opOnDocumentRestored(self, obj): + '''opOnDocumentRestored(obj) ... implement if an op needs special handling like migrating the data model. + Should be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def opOnChanged(self, obj, prop): + '''opOnChanged(obj, prop) ... overwrite to process property changes. + This is a callback function that is invoked each time a property of the + receiver is assigned a value. Note that the FC framework does not + distinguish between assigning a different value and assigning the same + value again. + Can safely be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def opSetDefaultValues(self, obj, job): + '''opSetDefaultValues(obj, job) ... overwrite to set initial default values. + Called after the receiver has been fully created with all properties. + Can safely be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def opUpdateDepths(self, obj): + '''opUpdateDepths(obj) ... overwrite to implement special depths calculation. + Can safely be overwritten by subclass.''' + pass # pylint: disable=unnecessary-pass + + def opExecute(self, obj): + '''opExecute(obj) ... called whenever the receiver needs to be recalculated. + See documentation of execute() for a list of base functionality provided. + Should be overwritten by subclasses.''' + pass # pylint: disable=unnecessary-pass + + def opRejectAddBase(self, obj, base, sub): + '''opRejectAddBase(base, sub) ... if op returns True the addition of the feature is prevented. + Should be overwritten by subclasses.''' + # pylint: disable=unused-argument + return False + + def onChanged(self, obj, prop): + '''onChanged(obj, prop) ... base implementation of the FC notification framework. + Do not overwrite, overwrite opOnChanged() instead.''' + if 'Restore' not in obj.State and prop in ['Base', 'StartDepth', 'FinalDepth']: + self.updateDepths(obj, True) + + self.opOnChanged(obj, prop) + + def applyExpression(self, obj, prop, expr): + '''applyExpression(obj, prop, expr) ... set expression expr on obj.prop if expr is set''' + if expr: + obj.setExpression(prop, expr) + return True + return False + + def setDefaultValues(self, obj): + '''setDefaultValues(obj) ... base implementation. + Do not overwrite, overwrite opSetDefaultValues() instead.''' + job = PathUtils.addToJob(obj) + + obj.Active = True + + features = self.opFeatures(obj) + + if FeatureTool & features: + if 1 < len(job.Operations.Group): + obj.ToolController = PathUtil.toolControllerForOp(job.Operations.Group[-2]) + else: + obj.ToolController = PathUtils.findToolController(obj) + if not obj.ToolController: + return None + obj.OpToolDiameter = obj.ToolController.Tool.Diameter + + if FeatureCoolant & features: + obj.CoolantMode = job.SetupSheet.CoolantMode + + if FeatureDepths & features: + if self.applyExpression(obj, 'StartDepth', job.SetupSheet.StartDepthExpression): + obj.OpStartDepth = 1.0 + else: + obj.StartDepth = 1.0 + if self.applyExpression(obj, 'FinalDepth', job.SetupSheet.FinalDepthExpression): + obj.OpFinalDepth = 0.0 + else: + obj.FinalDepth = 0.0 + else: + obj.StartDepth = 1.0 + + if FeatureStepDown & features: + if not self.applyExpression(obj, 'StepDown', job.SetupSheet.StepDownExpression): + obj.StepDown = '1 mm' + + if FeatureHeights & features: + if job.SetupSheet.SafeHeightExpression: + if not self.applyExpression(obj, 'SafeHeight', job.SetupSheet.SafeHeightExpression): + obj.SafeHeight = '3 mm' + if job.SetupSheet.ClearanceHeightExpression: + if not self.applyExpression(obj, 'ClearanceHeight', job.SetupSheet.ClearanceHeightExpression): + obj.ClearanceHeight = '5 mm' + + if FeatureStartPoint & features: + obj.UseStartPoint = False + + self.opSetDefaultValues(obj, job) + return job + + def _setBaseAndStock(self, obj, ignoreErrors=False): + job = PathUtils.findParentJob(obj) + if not job: + if not ignoreErrors: + PathLog.error(translate("Path", "No parent job found for operation.")) + return False + if not job.Model.Group: + if not ignoreErrors: + PathLog.error(translate("Path", "Parent job %s doesn't have a base object") % job.Label) + return False + self.job = job + self.model = job.Model.Group + self.stock = job.Stock + return True + + def getJob(self, obj): + '''getJob(obj) ... return the job this operation is part of.''' + if not hasattr(self, 'job') or self.job is None: + if not self._setBaseAndStock(obj): + return None + return self.job + + def updateDepths(self, obj, ignoreErrors=False): + '''updateDepths(obj) ... base implementation calculating depths depending on base geometry. + Should not be overwritten.''' + + def faceZmin(bb, fbb): + if fbb.ZMax == fbb.ZMin and fbb.ZMax == bb.ZMax: # top face + return fbb.ZMin + elif fbb.ZMax > fbb.ZMin and fbb.ZMax == bb.ZMax: # vertical face, full cut + return fbb.ZMin + elif fbb.ZMax > fbb.ZMin and fbb.ZMin > bb.ZMin: # internal vertical wall + return fbb.ZMin + elif fbb.ZMax == fbb.ZMin and fbb.ZMax > bb.ZMin: # face/shelf + return fbb.ZMin + return bb.ZMin + + if not self._setBaseAndStock(obj, ignoreErrors): + return False + + stockBB = self.stock.Shape.BoundBox + zmin = stockBB.ZMin + zmax = stockBB.ZMax + + obj.OpStockZMin = zmin + obj.OpStockZMax = zmax + + if hasattr(obj, 'Base') and obj.Base: + for base, sublist in obj.Base: + bb = base.Shape.BoundBox + zmax = max(zmax, bb.ZMax) + for sub in sublist: + try: + fbb = base.Shape.getElement(sub).BoundBox + zmin = max(zmin, faceZmin(bb, fbb)) + zmax = max(zmax, fbb.ZMax) + except Part.OCCError as e: + PathLog.error(e) + + else: + # clearing with stock boundaries + job = PathUtils.findParentJob(obj) + zmax = stockBB.ZMax + zmin = job.Proxy.modelBoundBox(job).ZMax + + if FeatureDepths & self.opFeatures(obj): + # first set update final depth, it's value is not negotiable + if not PathGeom.isRoughly(obj.OpFinalDepth.Value, zmin): + obj.OpFinalDepth = zmin + zmin = obj.OpFinalDepth.Value + + def minZmax(z): + if hasattr(obj, 'StepDown') and not PathGeom.isRoughly(obj.StepDown.Value, 0): + return z + obj.StepDown.Value + else: + return z + 1 + + # ensure zmax is higher than zmin + if (zmax - 0.0001) <= zmin: + zmax = minZmax(zmin) + + # update start depth if requested and required + if not PathGeom.isRoughly(obj.OpStartDepth.Value, zmax): + obj.OpStartDepth = zmax + else: + # every obj has a StartDepth + if obj.StartDepth.Value != zmax: + obj.StartDepth = zmax + + self.opUpdateDepths(obj) + + @waiting_effects + def execute(self, obj): + '''execute(obj) ... base implementation - do not overwrite! + Verifies that the operation is assigned to a job and that the job also has a valid Base. + It also sets the following instance variables that can and should be safely be used by + implementation of opExecute(): + self.model ... List of base objects of the Job itself + self.stock ... Stock object for the Job itself + self.vertFeed ... vertical feed rate of assigned tool + self.vertRapid ... vertical rapid rate of assigned tool + self.horizFeed ... horizontal feed rate of assigned tool + self.horizRapid ... norizontal rapid rate of assigned tool + self.tool ... the actual tool being used + self.radius ... the main radius of the tool being used + self.commandlist ... a list for collecting all commands produced by the operation + + Once everything is validated and above variables are set the implementation calls + opExecute(obj) - which is expected to add the generated commands to self.commandlist + Finally the base implementation adds a rapid move to clearance height and assigns + the receiver's Path property from the command list. + ''' + PathLog.track() + + if obj.ViewObject: + obj.ViewObject.Visibility = obj.Active + + if not obj.Active: + path = Path.Path("(inactive operation)") + obj.Path = path + return + + if not self._setBaseAndStock(obj): + return + + if FeatureCoolant & self.opFeatures(obj): + if not hasattr(obj, 'CoolantMode'): + PathLog.error(translate("Path", "No coolant property found. Please recreate operation.")) + + if FeatureTool & self.opFeatures(obj): + tc = obj.ToolController + if tc is None or tc.ToolNumber == 0: + PathLog.error(translate("Path", "No Tool Controller is selected. We need a tool to build a Path.")) + return + else: + self.vertFeed = tc.VertFeed.Value + self.horizFeed = tc.HorizFeed.Value + self.vertRapid = tc.VertRapid.Value + self.horizRapid = tc.HorizRapid.Value + tool = tc.Proxy.getTool(tc) + if not tool or float(tool.Diameter) == 0: + PathLog.error(translate("Path", "No Tool found or diameter is zero. We need a tool to build a Path.")) + return + self.radius = float(tool.Diameter) / 2.0 + self.tool = tool + obj.OpToolDiameter = tool.Diameter + + self.updateDepths(obj) + # now that all op values are set make sure the user properties get updated accordingly, + # in case they still have an expression referencing any op values + obj.recompute() + + self.commandlist = [] + self.commandlist.append(Path.Command("(%s)" % obj.Label)) + if obj.Comment: + self.commandlist.append(Path.Command("(%s)" % obj.Comment)) + + result = self.opExecute(obj) # pylint: disable=assignment-from-no-return + + if FeatureHeights & self.opFeatures(obj): + # Let's finish by rapid to clearance...just for safety + self.commandlist.append(Path.Command("G0", {"Z": obj.ClearanceHeight.Value})) + + path = Path.Path(self.commandlist) + obj.Path = path + obj.CycleTime = self.getCycleTimeEstimate(obj) + self.job.Proxy.getCycleTime() + return result + + def getCycleTimeEstimate(self, obj): + + tc = obj.ToolController + + if tc is None or tc.ToolNumber == 0: + PathLog.error(translate("Path", "No Tool Controller selected.")) + return translate('Path', 'Tool Error') + + hFeedrate = tc.HorizFeed.Value + vFeedrate = tc.VertFeed.Value + hRapidrate = tc.HorizRapid.Value + vRapidrate = tc.VertRapid.Value + + if hFeedrate == 0 or vFeedrate == 0: + PathLog.warning(translate("Path", "Tool Controller feedrates required to calculate the cycle time.")) + return translate('Path', 'Feedrate Error') + + if hRapidrate == 0 or vRapidrate == 0: + PathLog.warning(translate("Path", "Add Tool Controller Rapid Speeds on the SetupSheet for more accurate cycle times.")) + + # Get the cycle time in seconds + seconds = obj.Path.getCycleTime(hFeedrate, vFeedrate, hRapidrate, vRapidrate) + + if not seconds: + return translate('Path', 'Cycletime Error') + + # Convert the cycle time to a HH:MM:SS format + cycleTime = time.strftime("%H:%M:%S", time.gmtime(seconds)) + + return cycleTime + + def addBase(self, obj, base, sub): + PathLog.track(obj, base, sub) + base = PathUtil.getPublicObject(base) + + if self._setBaseAndStock(obj): + for model in self.job.Model.Group: + if base == self.job.Proxy.baseObject(self.job, model): + base = model + break + + baselist = obj.Base + if baselist is None: + baselist = [] + + for p, el in baselist: + if p == base and sub in el: + PathLog.notice((translate("Path", "Base object %s.%s already in the list") + "\n") % (base.Label, sub)) + return + + if not self.opRejectAddBase(obj, base, sub): + baselist.append((base, sub)) + obj.Base = baselist + else: + PathLog.notice((translate("Path", "Base object %s.%s rejected by operation") + "\n") % (base.Label, sub)) From 4396789b042c2a60f25779656a95cf47293c12b1 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 7 May 2020 23:19:21 -0500 Subject: [PATCH 10/11] Path: Improve geometry selection and `Cancel` operation error messages --- src/Mod/Path/PathScripts/PathOpGui.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathOpGui.py b/src/Mod/Path/PathScripts/PathOpGui.py index 2ef8264d81..c2098c7ab1 100644 --- a/src/Mod/Path/PathScripts/PathOpGui.py +++ b/src/Mod/Path/PathScripts/PathOpGui.py @@ -470,7 +470,9 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): def selectionSupportedAsBaseGeometry(self, selection, ignoreErrors): if len(selection) != 1: if not ignoreErrors: - PathLog.error(translate("PathProject", "Please select %s from a single solid" % self.featureName())) + msg = translate("PathProject", "Please select %s from a single solid" % self.featureName()) + FreeCAD.Console.PrintError(msg + '\n') + PathLog.debug(msg) return False sel = selection[0] if sel.HasSubObjects: @@ -506,16 +508,16 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): if self.addBaseGeometry(FreeCADGui.Selection.getSelectionEx()): # self.obj.Proxy.execute(self.obj) self.setFields(self.obj) - self.updatePanelVisibility('Operation', self.obj) self.setDirty() + self.updatePanelVisibility('Operation', self.obj) def deleteBase(self): PathLog.track() selected = self.form.baseList.selectedItems() for item in selected: self.form.baseList.takeItem(self.form.baseList.row(item)) - self.updatePanelVisibility('Operation', self.obj) self.setDirty() + self.updatePanelVisibility('Operation', self.obj) self.updateBase() # self.obj.Proxy.execute(self.obj) # FreeCAD.ActiveDocument.recompute() @@ -537,8 +539,8 @@ class TaskPanelBaseGeometryPage(TaskPanelPage): def clearBase(self): self.obj.Base = [] - self.updatePanelVisibility('Operation', self.obj) self.setDirty() + self.updatePanelVisibility('Operation', self.obj) def registerSignalHandlers(self, obj): self.form.baseList.itemSelectionChanged.connect(self.itemActivated) @@ -975,8 +977,11 @@ class TaskPanel(object): FreeCAD.ActiveDocument.abortTransaction() if self.deleteOnReject: FreeCAD.ActiveDocument.openTransaction(translate("Path", "Uncreate AreaOp Operation")) - PathUtil.clearExpressionEngine(self.obj) - FreeCAD.ActiveDocument.removeObject(self.obj.Name) + try: + PathUtil.clearExpressionEngine(self.obj) + FreeCAD.ActiveDocument.removeObject(self.obj.Name) + except Exception as ee: + PathLog.debug('{}\n'.format(ee)) FreeCAD.ActiveDocument.commitTransaction() self.cleanup(resetEdit) return True From e835bf45a747850002b79f86a119449578b1aee7 Mon Sep 17 00:00:00 2001 From: Russell Johnson <47639332+Russ4262@users.noreply.github.com> Date: Thu, 7 May 2020 23:20:26 -0500 Subject: [PATCH 11/11] Path: Update selection gates pertaining to unified `Profile` operation --- src/Mod/Path/PathScripts/PathSelection.py | 81 ++++++++++++++++++----- 1 file changed, 66 insertions(+), 15 deletions(-) diff --git a/src/Mod/Path/PathScripts/PathSelection.py b/src/Mod/Path/PathScripts/PathSelection.py index e34f03e699..943d6ae28a 100644 --- a/src/Mod/Path/PathScripts/PathSelection.py +++ b/src/Mod/Path/PathScripts/PathSelection.py @@ -102,8 +102,29 @@ class DRILLGate(PathBaseGate): return False -class PROFILEGate(PathBaseGate): +class FACEGate(PathBaseGate): # formerly PROFILEGate class using allow_ORIG method as allow() def allow(self, doc, obj, sub): # pylint: disable=unused-argument + profileable = False + + try: + obj = obj.Shape + except Exception: # pylint: disable=broad-except + return False + + if obj.ShapeType == 'Compound': + if sub and sub[0:4] == 'Face': + profileable = True + + elif obj.ShapeType == 'Face': # 3D Face, not flat, planar? + profileable = True # Was False + + elif obj.ShapeType == 'Solid': + if sub and sub[0:4] == 'Face': + profileable = True + + return profileable + + def allow_ORIG(self, doc, obj, sub): # pylint: disable=unused-argument profileable = False try: @@ -137,6 +158,33 @@ class PROFILEGate(PathBaseGate): return profileable +class PROFILEGate(PathBaseGate): + def allow(self, doc, obj, sub): # pylint: disable=unused-argument + if sub and sub[0:4] == 'Edge': + return True + + try: + obj = obj.Shape + except Exception: # pylint: disable=broad-except + return False + + if obj.ShapeType == 'Compound': + if sub and sub[0:4] == 'Face': + return True + + elif obj.ShapeType == 'Face': + return True + + elif obj.ShapeType == 'Solid': + if sub and sub[0:4] == 'Face': + return True + + elif obj.ShapeType == 'Wire': + return True + + return False + + class POCKETGate(PathBaseGate): def allow(self, doc, obj, sub): # pylint: disable=unused-argument @@ -179,10 +227,12 @@ class CONTOURGate(PathBaseGate): def allow(self, doc, obj, sub): # pylint: disable=unused-argument pass + class PROBEGate: def allow(self, doc, obj, sub): pass + def contourselect(): FreeCADGui.Selection.addSelectionGate(CONTOURGate()) FreeCAD.Console.PrintWarning("Contour Select Mode\n") @@ -203,17 +253,18 @@ def engraveselect(): FreeCAD.Console.PrintWarning("Engraving Select Mode\n") +def fselect(): + FreeCADGui.Selection.addSelectionGate(FACEGate()) # Was PROFILEGate() + FreeCAD.Console.PrintWarning("Profiling Select Mode\n") + + def chamferselect(): FreeCADGui.Selection.addSelectionGate(CHAMFERGate()) FreeCAD.Console.PrintWarning("Deburr Select Mode\n") def profileselect(): - gate = False - if(PROFILEGate() or EGate()): - gate = True - FreeCADGui.Selection.addSelectionGate(gate) - # FreeCADGui.Selection.addSelectionGate(PROFILEGate()) + FreeCADGui.Selection.addSelectionGate(PROFILEGate()) FreeCAD.Console.PrintWarning("Profiling Select Mode\n") @@ -228,21 +279,21 @@ def adaptiveselect(): def surfaceselect(): - if(MESHGate() is True or PROFILEGate() is True): - FreeCADGui.Selection.addSelectionGate(True) - else: - FreeCADGui.Selection.addSelectionGate(False) - # FreeCADGui.Selection.addSelectionGate(MESHGate()) - # FreeCADGui.Selection.addSelectionGate(PROFILEGate()) # Added for face selection + gate = False + if(MESHGate() or FACEGate()): + gate = True + FreeCADGui.Selection.addSelectionGate(gate) FreeCAD.Console.PrintWarning("Surfacing Select Mode\n") + def probeselect(): FreeCADGui.Selection.addSelectionGate(PROBEGate()) FreeCAD.Console.PrintWarning("Probe Select Mode\n") + def select(op): opsel = {} - opsel['Contour'] = contourselect + opsel['Contour'] = contourselect # (depreciated) opsel['Deburr'] = chamferselect opsel['Drilling'] = drillselect opsel['Engrave'] = engraveselect @@ -251,8 +302,8 @@ def select(op): opsel['Pocket'] = pocketselect opsel['Pocket 3D'] = pocketselect opsel['Pocket Shape'] = pocketselect - opsel['Profile Edges'] = eselect - opsel['Profile Faces'] = profileselect + opsel['Profile Edges'] = eselect # (depreciated) + opsel['Profile Faces'] = fselect # (depreciated) opsel['Profile'] = profileselect opsel['Surface'] = surfaceselect opsel['Waterline'] = surfaceselect