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] 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")