| | |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | from PySide.QtCore import QT_TRANSLATE_NOOP |
| | import FreeCAD |
| | import Part |
| | import Path |
| | import math |
| |
|
| | |
| | from lazy_loader.lazy_loader import LazyLoader |
| |
|
| | PathUtils = LazyLoader("PathScripts.PathUtils", globals(), "PathScripts.PathUtils") |
| |
|
| |
|
| | __title__ = "CAM Features Extensions" |
| | __author__ = "sliptonic (Brad Collette)" |
| | __url__ = "https://www.freecad.org" |
| | __doc__ = "Class and implementation of face extensions features." |
| |
|
| |
|
| | if False: |
| | Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) |
| | Path.Log.trackModule(Path.Log.thisModule()) |
| | else: |
| | Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) |
| |
|
| | translate = FreeCAD.Qt.translate |
| |
|
| |
|
| | def endPoints(edgeOrWire): |
| | """endPoints(edgeOrWire) ... return the first and last point of the wire or the edge, assuming the argument is not a closed wire.""" |
| | if Part.Wire == type(edgeOrWire): |
| | |
| | pts = [e.valueAt(e.FirstParameter) for e in edgeOrWire.Edges] |
| | pts.extend([e.valueAt(e.LastParameter) for e in edgeOrWire.Edges]) |
| | unique = [] |
| | for p in pts: |
| | cnt = len([p2 for p2 in pts if Path.Geom.pointsCoincide(p, p2)]) |
| | if 1 == cnt: |
| | unique.append(p) |
| |
|
| | return unique |
| |
|
| | pfirst = edgeOrWire.valueAt(edgeOrWire.FirstParameter) |
| | plast = edgeOrWire.valueAt(edgeOrWire.LastParameter) |
| | if Path.Geom.pointsCoincide(pfirst, plast): |
| | return None |
| |
|
| | return [pfirst, plast] |
| |
|
| |
|
| | def includesPoint(p, pts): |
| | """includesPoint(p, pts) ... answer True if the collection of pts includes the point p""" |
| | for pt in pts: |
| | if Path.Geom.pointsCoincide(p, pt): |
| | return True |
| |
|
| | return False |
| |
|
| |
|
| | def selectOffsetWire(feature, wires): |
| | """selectOffsetWire(feature, wires) ... returns the Wire in wires which is does not intersect with feature""" |
| | closest = None |
| | for w in wires: |
| | dist = feature.distToShape(w)[0] |
| | if closest is None or dist > closest[0]: |
| | closest = (dist, w) |
| |
|
| | if closest is not None: |
| | return closest[1] |
| |
|
| | return None |
| |
|
| |
|
| | def extendWire(feature, wire, length): |
| | """extendWire(wire, length) ... return a closed Wire which extends wire by length""" |
| | Path.Log.track(length) |
| |
|
| | if not length or length == 0: |
| | return None |
| |
|
| | try: |
| | off2D = wire.makeOffset2D(length) |
| | except FreeCAD.Base.FreeCADError as ee: |
| | Path.Log.debug(ee) |
| | return None |
| | endPts = endPoints(wire) |
| | if endPts: |
| | edges = [ |
| | e |
| | for e in off2D.Edges |
| | if Part.Circle != type(e.Curve) or not includesPoint(e.Curve.Center, endPts) |
| | ] |
| | wires = [Part.Wire(e) for e in Part.sortEdges(edges)] |
| | offset = selectOffsetWire(feature, wires) |
| | ePts = endPoints(offset) |
| | if ePts and len(ePts) > 1: |
| | l0 = (ePts[0] - endPts[0]).Length |
| | l1 = (ePts[1] - endPts[0]).Length |
| | edges = wire.Edges |
| | if l0 < l1: |
| | edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[0]))) |
| | edges.extend(offset.Edges) |
| | edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[1]))) |
| | else: |
| | edges.append(Part.Edge(Part.LineSegment(endPts[1], ePts[0]))) |
| | edges.extend(offset.Edges) |
| | edges.append(Part.Edge(Part.LineSegment(endPts[0], ePts[1]))) |
| |
|
| | return Part.Wire(edges) |
| |
|
| | return None |
| |
|
| |
|
| | def createExtension(obj, extObj, extFeature, extSub): |
| | return Extension( |
| | obj, |
| | extObj, |
| | extFeature, |
| | extSub, |
| | obj.ExtensionLengthDefault, |
| | Extension.DirectionNormal, |
| | ) |
| |
|
| |
|
| | def readObjExtensionFeature(obj): |
| | """readObjExtensionFeature(obj)... |
| | Return three item string tuples (base name, feature, subfeature) extracted from obj.ExtensionFeature |
| | """ |
| | extensions = [] |
| |
|
| | for extObj, features in obj.ExtensionFeature: |
| | for sub in features: |
| | extFeature, extSub = sub.split(":") |
| | extensions.append((extObj.Name, extFeature, extSub)) |
| | return extensions |
| |
|
| |
|
| | def getExtensions(obj): |
| | Path.Log.debug("getExtenstions()") |
| | extensions = [] |
| | i = 0 |
| |
|
| | for extObj, features in obj.ExtensionFeature: |
| | for sub in features: |
| | extFeature, extSub = sub.split(":") |
| | extensions.append(createExtension(obj, extObj, extFeature, extSub)) |
| | i = i + 1 |
| | return extensions |
| |
|
| |
|
| | def setExtensions(obj, extensions): |
| | Path.Log.track(obj.Label, len(extensions)) |
| | obj.ExtensionFeature = [(ext.obj, ext.getSubLink()) for ext in extensions] |
| |
|
| |
|
| | def getStandardAngle(x, y): |
| | """getStandardAngle(x, y)... |
| | Return standard degree angle given x and y values of vector.""" |
| | angle = math.degrees(math.atan2(y, x)) |
| | if angle < 0.0: |
| | return angle + 360.0 |
| | return angle |
| |
|
| |
|
| | def arcAdjustmentAngle(arc1, arc2): |
| | """arcAdjustmentAngle(arc1, arc2)... |
| | Return adjustment angle to apply to arc2 in order to align it with arc1. |
| | Arcs must have same center point.""" |
| | center = arc1.Curve.Center |
| | cntr2 = arc2.Curve.Center |
| |
|
| | |
| | if center.sub(cntr2).Length > 0.0000001: |
| | return None |
| |
|
| | |
| | midPntArc1 = arc1.valueAt( |
| | arc1.FirstParameter + (arc1.LastParameter - arc1.FirstParameter) / 2.0 |
| | ) |
| | midPntVect1 = midPntArc1.sub(center) |
| | ang1 = getStandardAngle(midPntVect1.x, midPntVect1.y) |
| |
|
| | |
| | midPntArc2 = arc2.valueAt( |
| | arc2.FirstParameter + (arc2.LastParameter - arc2.FirstParameter) / 2.0 |
| | ) |
| | midPntVect2 = midPntArc2.sub(center) |
| | ang2 = getStandardAngle(midPntVect2.x, midPntVect2.y) |
| |
|
| | |
| | return ang1 - ang2 |
| |
|
| |
|
| | class Extension(object): |
| | DirectionNormal = 0 |
| | DirectionX = 1 |
| | DirectionY = 2 |
| |
|
| | def __init__(self, op, obj, feature, sub, length, direction): |
| | Path.Log.debug( |
| | "Extension(%s, %s, %s, %.2f, %s" % (obj.Label, feature, sub, length, direction) |
| | ) |
| | self.op = op |
| | self.obj = obj |
| | self.feature = feature |
| | self.sub = sub |
| | self.length = length |
| | self.direction = direction |
| | self.extFaces = None |
| | self.isDebug = True if Path.Log.getLevel(Path.Log.thisModule()) == 4 else False |
| |
|
| | self.avoid = False |
| | if sub.startswith("Avoid_"): |
| | self.avoid = True |
| |
|
| | self.wire = None |
| |
|
| | def getSubLink(self): |
| | return "%s:%s" % (self.feature, self.sub) |
| |
|
| | def _extendEdge(self, feature, e0, direction): |
| | Path.Log.track(feature, e0, direction) |
| | if isinstance(e0.Curve, Part.Line) or isinstance(e0.Curve, Part.LineSegment): |
| | e2 = e0.copy() |
| | off = self.length.Value * direction |
| | e2.translate(off) |
| | e2 = Path.Geom.flipEdge(e2) |
| | e1 = Part.Edge( |
| | Part.LineSegment(e0.valueAt(e0.LastParameter), e2.valueAt(e2.FirstParameter)) |
| | ) |
| | e3 = Part.Edge( |
| | Part.LineSegment(e2.valueAt(e2.LastParameter), e0.valueAt(e0.FirstParameter)) |
| | ) |
| | wire = Part.Wire([e0, e1, e2, e3]) |
| | self.wire = wire |
| | return wire |
| |
|
| | return extendWire(feature, Part.Wire([e0]), self.length.Value) |
| |
|
| | def _getEdgeNumbers(self): |
| | if "Wire" in self.sub: |
| | numbers = [nr for nr in self.sub[5:-1].split(",")] |
| | else: |
| | numbers = [self.sub[4:]] |
| |
|
| | Path.Log.debug("_getEdgeNumbers() -> %s" % numbers) |
| | return numbers |
| |
|
| | def _getEdgeNames(self): |
| | return ["Edge%s" % nr for nr in self._getEdgeNumbers()] |
| |
|
| | def _getEdges(self): |
| | return [self.obj.Shape.getElement(sub) for sub in self._getEdgeNames()] |
| |
|
| | def _getDirectedNormal(self, p0, normal): |
| | poffPlus = p0 + 0.01 * normal |
| | poffMinus = p0 - 0.01 * normal |
| | if not self.obj.Shape.isInside(poffPlus, 0.005, True): |
| | return normal |
| |
|
| | if not self.obj.Shape.isInside(poffMinus, 0.005, True): |
| | return normal.negative() |
| |
|
| | return None |
| |
|
| | def _getDirection(self, wire): |
| | e0 = wire.Edges[0] |
| | midparam = e0.FirstParameter + 0.5 * (e0.LastParameter - e0.FirstParameter) |
| | tangent = e0.tangentAt(midparam) |
| | Path.Log.track("tangent", tangent, self.feature, self.sub) |
| | normal = tangent.cross(FreeCAD.Vector(0, 0, 1)) |
| | if Path.Geom.pointsCoincide(normal, FreeCAD.Vector(0, 0, 0)): |
| | return None |
| |
|
| | return self._getDirectedNormal(e0.valueAt(midparam), normal.normalize()) |
| |
|
| | def getExtensionFaces(self, extensionWire): |
| | """getExtensionFace(extensionWire)... |
| | A public helper method to retrieve the requested extension as a face, |
| | rather than a wire because some extensions require a face shape |
| | for definition that allows for two wires for boundary definition. |
| | """ |
| |
|
| | if self.extFaces: |
| | return self.extFaces |
| |
|
| | return [Part.Face(extensionWire)] |
| |
|
| | def getWire(self): |
| | """getWire()... Public method to retrieve the extension area, pertaining to the feature |
| | and sub element provided at class instantiation, as a closed wire. If no closed wire |
| | is possible, a `None` value is returned.""" |
| |
|
| | return self._getRegularWire() |
| |
|
| | def _getRegularWire(self): |
| | """_getRegularWire()... Private method to retrieve the extension area, pertaining to the feature |
| | and sub element provided at class instantiation, as a closed wire. If no closed wire |
| | is possible, a `None` value is returned.""" |
| | Path.Log.track() |
| |
|
| | length = self.length.Value |
| | if Path.Geom.isRoughly(0, length) or not self.sub: |
| | Path.Log.debug("no extension, length=%.2f, sub=%s" % (length, self.sub)) |
| | return None |
| |
|
| | feature = self.obj.Shape.getElement(self.feature) |
| | edges = self._getEdges() |
| | sub = Part.Wire(Part.sortEdges(edges)[0]) |
| |
|
| | if 1 == len(edges): |
| | Path.Log.debug("Extending single edge wire") |
| | edge = edges[0] |
| | if Part.Circle == type(edge.Curve): |
| | Path.Log.debug("is Part.Circle") |
| | circle = edge.Curve |
| | |
| | p0 = edge.valueAt(edge.FirstParameter) |
| | normal = (edge.Curve.Center - p0).normalize() |
| | direction = self._getDirectedNormal(p0, normal) |
| | if direction is None: |
| | return None |
| |
|
| | if Path.Geom.pointsCoincide(normal, direction): |
| | r = circle.Radius - length |
| | else: |
| | r = circle.Radius + length |
| |
|
| | |
| | if r > 0: |
| | Path.Log.debug("radius > 0 - extend outward") |
| | e3 = Part.makeCircle( |
| | r, |
| | circle.Center, |
| | circle.Axis, |
| | edge.FirstParameter * 180 / math.pi, |
| | edge.LastParameter * 180 / math.pi, |
| | ) |
| |
|
| | |
| | rotationAdjustment = arcAdjustmentAngle(edge, e3) |
| | if not Path.Geom.isRoughly(rotationAdjustment, 0.0): |
| | e3.rotate( |
| | edge.Curve.Center, |
| | FreeCAD.Vector(0.0, 0.0, 1.0), |
| | rotationAdjustment, |
| | ) |
| |
|
| | if endPoints(edge): |
| | Path.Log.debug("Make section of donut") |
| | |
| | e0 = Part.makeLine( |
| | edge.valueAt(edge.FirstParameter), |
| | e3.valueAt(e3.FirstParameter), |
| | ) |
| | e2 = Part.makeLine( |
| | edge.valueAt(edge.LastParameter), |
| | e3.valueAt(e3.LastParameter), |
| | ) |
| |
|
| | wire = Part.Wire([e0, edge, e2, e3]) |
| |
|
| | |
| | face = Part.Face(wire) |
| | if face.common(feature).Area < face.Area * 0.10: |
| | return wire |
| | else: |
| | return None |
| |
|
| | extWire = Part.Wire([e3]) |
| | self.extFaces = [self._makeCircularExtFace(edge, extWire)] |
| | return extWire |
| |
|
| | Path.Log.debug("radius < 0 - extend inward") |
| | |
| | if endPoints(edge): |
| | |
| | Path.Log.track() |
| | center = circle.Center |
| | e0 = Part.makeLine(center, edge.valueAt(edge.FirstParameter)) |
| | e2 = Part.makeLine(edge.valueAt(edge.LastParameter), center) |
| | return Part.Wire([e0, edge, e2]) |
| |
|
| | Path.Log.track() |
| | return Part.Wire([edge]) |
| |
|
| | else: |
| | Path.Log.debug("else is NOT Part.Circle") |
| | Path.Log.track(self.feature, self.sub, type(edge.Curve), endPoints(edge)) |
| | direction = self._getDirection(sub) |
| | if direction is None: |
| | return None |
| |
|
| | return self._extendEdge(feature, edges[0], direction) |
| |
|
| | elif sub.isClosed(): |
| | Path.Log.debug("Extending multi-edge closed wire") |
| | subFace = Part.Face(sub) |
| | featFace = Part.Face(feature.Wires[0]) |
| | isOutside = True |
| | if not Path.Geom.isRoughly(featFace.Area, subFace.Area): |
| | length = -1.0 * length |
| | isOutside = False |
| |
|
| | try: |
| | off2D = sub.makeOffset2D(length) |
| | except FreeCAD.Base.FreeCADError as ee: |
| | Path.Log.debug(ee) |
| | return None |
| |
|
| | if isOutside: |
| | self.extFaces = [Part.Face(off2D).cut(featFace)] |
| | else: |
| | self.extFaces = [subFace.cut(Part.Face(off2D))] |
| | return off2D |
| |
|
| | Path.Log.debug("Extending multi-edge open wire") |
| | extendedWire = extendWire(feature, sub, length) |
| | if extendedWire is None: |
| | return extendedWire |
| |
|
| | |
| | extFace = Part.Face(extendedWire) |
| | trimmedWire = extFace.cut(self.obj.Shape).Wires[0] |
| | return trimmedWire.copy() |
| |
|
| | def _makeCircularExtFace(self, edge, extWire): |
| | """_makeCircularExtensionFace(edge, extWire)... |
| | Create proper circular extension face shape. Incoming edge is expected to be a circle. |
| | """ |
| | |
| | edgeFace = Part.Face(Part.Wire([edge])) |
| | edgeFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - edgeFace.BoundBox.ZMin)) |
| | extWireFace = Part.Face(extWire) |
| | extWireFace.translate(FreeCAD.Vector(0.0, 0.0, 0.0 - extWireFace.BoundBox.ZMin)) |
| |
|
| | if extWireFace.Area >= edgeFace.Area: |
| | extensionFace = extWireFace.cut(edgeFace) |
| | else: |
| | extensionFace = edgeFace.cut(extWireFace) |
| | extensionFace.translate(FreeCAD.Vector(0.0, 0.0, edge.BoundBox.ZMin)) |
| |
|
| | return extensionFace |
| |
|
| |
|
| | |
| |
|
| |
|
| | def initialize_properties(obj): |
| | """initialize_properties(obj)... Adds feature properties to object argument""" |
| | if not hasattr(obj, "ExtensionLengthDefault"): |
| | obj.addProperty( |
| | "App::PropertyDistance", |
| | "ExtensionLengthDefault", |
| | "Extension", |
| | QT_TRANSLATE_NOOP("App::Property", "Default length of extensions."), |
| | ) |
| | if not hasattr(obj, "ExtensionFeature"): |
| | obj.addProperty( |
| | "App::PropertyLinkSubListGlobal", |
| | "ExtensionFeature", |
| | "Extension", |
| | QT_TRANSLATE_NOOP("App::Property", "List of features to extend."), |
| | ) |
| | if not hasattr(obj, "ExtensionCorners"): |
| | obj.addProperty( |
| | "App::PropertyBool", |
| | "ExtensionCorners", |
| | "Extension", |
| | QT_TRANSLATE_NOOP( |
| | "App::Property", |
| | "When enabled connected extension edges are combined to wires.", |
| | ), |
| | ) |
| | obj.ExtensionCorners = True |
| |
|
| | obj.setEditorMode("ExtensionFeature", 2) |
| |
|
| |
|
| | def set_default_property_values(obj, job): |
| | """set_default_property_values(obj, job) ... set default values for feature properties""" |
| | obj.ExtensionCorners = True |
| | obj.setExpression("ExtensionLengthDefault", "OpToolDiameter / 2.0") |
| |
|
| |
|
| | def SetupProperties(): |
| | """SetupProperties()... Returns list of feature property names""" |
| | setup = ["ExtensionLengthDefault", "ExtensionFeature", "ExtensionCorners"] |
| | return setup |
| |
|
| |
|
| | |
| | def getExtendOutlineFace(base_shape, face, extension, remHoles=False, offset_tolerance=1e-4): |
| | """getExtendOutlineFace(obj, base_shape, face, extension, remHoles) ... |
| | Creates an extended face for the pocket, taking into consideration lateral |
| | collision with the greater base shape. |
| | Arguments are: |
| | parent base shape of face, |
| | target face, |
| | extension magnitude, |
| | remove holes boolean, |
| | offset tolerance = 1e-4 default |
| | The default value of 1e-4 for offset tolerance is the same default value |
| | at getOffsetArea() function definition. |
| | Return is an all access face extending the specified extension value from the source face. |
| | """ |
| |
|
| | |
| | offset_face = PathUtils.getOffsetArea( |
| | face, extension, removeHoles=remHoles, plane=face, tolerance=offset_tolerance |
| | ) |
| | if not offset_face: |
| | Path.Log.error("Failed to offset a selected face.") |
| | return None |
| |
|
| | |
| | depth = 0.2 |
| | offset_ext = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, depth)) |
| | face_del = offset_face.extrude(FreeCAD.Vector(0.0, 0.0, -1.0 * depth)) |
| | clear = base_shape.cut(face_del) |
| | available = offset_ext.cut(clear) |
| | available.removeSplitter() |
| |
|
| | |
| | |
| | |
| |
|
| | |
| | zmin = available.BoundBox.ZMax |
| | bottom_faces = list() |
| | for f in available.Faces: |
| | bbx = f.BoundBox |
| | zNorm = abs(f.normalAt(0.0, 0.0).z) |
| | if ( |
| | Path.Geom.isRoughly(zNorm, 1.0) |
| | and Path.Geom.isRoughly(bbx.ZMax - bbx.ZMin, 0.0) |
| | and Path.Geom.isRoughly(bbx.ZMin, face.BoundBox.ZMin) |
| | ): |
| | if bbx.ZMin < zmin: |
| | bottom_faces.append(f) |
| |
|
| | if bottom_faces: |
| | extended = None |
| | for bf in bottom_faces: |
| | |
| | diff = face.BoundBox.ZMax - bf.BoundBox.ZMax |
| | bf.translate(FreeCAD.Vector(0.0, 0.0, diff)) |
| | cmn = bf.common(face) |
| | if hasattr(cmn, "Area") and cmn.Area > 0.0: |
| | extended = bf |
| |
|
| | return extended |
| |
|
| | Path.Log.error("No bottom face for extend outline.") |
| | return None |
| |
|
| |
|
| | |
| | def getWaterlineFace(base_shape, face): |
| | """getWaterlineFace(base_shape, face) ... |
| | Creates a waterline extension face for the target face, |
| | taking into consideration the greater base shape. |
| | Arguments are: parent base shape and target face. |
| | Return is a waterline face at height of the target face. |
| | """ |
| | faceHeight = face.BoundBox.ZMin |
| |
|
| | |
| | baseBB = base_shape.BoundBox |
| | depthparams = PathUtils.depth_params( |
| | clearance_height=faceHeight, |
| | safe_height=faceHeight, |
| | start_depth=faceHeight, |
| | step_down=math.floor(faceHeight - baseBB.ZMin + 2.0), |
| | z_finish_step=0.0, |
| | final_depth=baseBB.ZMin, |
| | user_depths=None, |
| | ) |
| | env = PathUtils.getEnvelope(partshape=base_shape, subshape=None, depthparams=depthparams) |
| | |
| | rawList = list() |
| | for f in env.Faces: |
| | if Path.Geom.isRoughly(f.BoundBox.ZMin, faceHeight): |
| | rawList.append(f) |
| | |
| | rawComp = Part.makeCompound(rawList) |
| | rawCompExtNeg = rawComp.extrude(FreeCAD.Vector(0.0, 0.0, baseBB.ZMin - faceHeight - 1.0)) |
| | |
| | topSolid = base_shape.cut(rawCompExtNeg) |
| |
|
| | |
| | |
| | |
| | |
| | |
| |
|
| | |
| | waterlineShape = rawComp.cut(topSolid) |
| | faces = list() |
| | for f in waterlineShape.Faces: |
| | cmn = face.common(f) |
| | if hasattr(cmn, "Area") and cmn.Area > 0.0: |
| | faces.append(f) |
| | if faces: |
| | return Part.makeCompound(faces) |
| |
|
| | return None |
| |
|