Download src/Mod/CAM/Path/Op/FeatureExtension.py from AbdulElahGwaith/FreeCAD: direct link, hf CLI and curl.
- Browser
- Download file 23.4 kB
-
https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/FeatureExtension.py
- Command line
-
hf download hf://AbdulElahGwaith/FreeCAD/src/Mod/CAM/Path/Op/FeatureExtension.py
-
curl -L -o FeatureExtension.py https://huggingface.co/AbdulElahGwaith/FreeCAD/resolve/main/src/Mod/CAM/Path/Op/FeatureExtension.py
23.4 kB
| # SPDX-License-Identifier: LGPL-2.1-or-later | |
| # *************************************************************************** | |
| # * Copyright (c) 2019 sliptonic <shopinthewoods@gmail.com> * | |
| # * * | |
| # * 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 * | |
| # * * | |
| # *************************************************************************** | |
| from PySide.QtCore import QT_TRANSLATE_NOOP | |
| import FreeCAD | |
| import Part | |
| import Path | |
| import math | |
| # lazily loaded modules | |
| 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): | |
| # edges = edgeOrWire.Edges | |
| 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) # Assumes wire is NOT closed | |
| 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 | |
| # Verify centers of arcs are same | |
| if center.sub(cntr2).Length > 0.0000001: | |
| return None | |
| # Calculate midpoint of arc1, and standard angle from center to that midpoint | |
| midPntArc1 = arc1.valueAt( | |
| arc1.FirstParameter + (arc1.LastParameter - arc1.FirstParameter) / 2.0 | |
| ) | |
| midPntVect1 = midPntArc1.sub(center) | |
| ang1 = getStandardAngle(midPntVect1.x, midPntVect1.y) | |
| # Calculate midpoint of arc2, and standard angle from center to that midpoint | |
| midPntArc2 = arc2.valueAt( | |
| arc2.FirstParameter + (arc2.LastParameter - arc2.FirstParameter) / 2.0 | |
| ) | |
| midPntVect2 = midPntArc2.sub(center) | |
| ang2 = getStandardAngle(midPntVect2.x, midPntVect2.y) | |
| # Return adjustment angle to apply to arc2 in order to align with arc1 | |
| 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 | |
| # for a circle we have to figure out if it's a hole or a cylinder | |
| 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 | |
| # assuming the offset produces a valid circle - go for it | |
| 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, | |
| ) | |
| # Determine if rotational alignment is necessary for new arc | |
| 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") | |
| # need to construct the arc slice | |
| 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]) | |
| # Determine if calculated extension collides with model (wrong direction) | |
| face = Part.Face(wire) | |
| if face.common(feature).Area < face.Area * 0.10: | |
| return wire # Calculated extension is correct | |
| else: | |
| return None # Extension collides with model | |
| extWire = Part.Wire([e3]) | |
| self.extFaces = [self._makeCircularExtFace(edge, extWire)] | |
| return extWire | |
| Path.Log.debug("radius < 0 - extend inward") | |
| # the extension is bigger than the hole - so let's just cover the whole hole | |
| if endPoints(edge): | |
| # if the resulting arc is smaller than the radius, create a pie slice | |
| 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 | |
| # Trim wire face using model | |
| 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. | |
| """ | |
| # Add original outer wire to cut faces if necessary | |
| 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 | |
| # Eclass | |
| 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 | |
| # Extend outline face generation function | |
| 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. | |
| """ | |
| # Make offset face per user-specified extension distance so as to allow full clearing of face where possible. | |
| 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 | |
| # Apply collision detection by limiting extended face using base shape | |
| 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() | |
| # Debug | |
| # Part.show(available) | |
| # FreeCAD.ActiveDocument.ActiveObject.Label = "available" | |
| # Identify bottom face of available volume | |
| 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: | |
| # Drop travel face to same height as source face | |
| 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 | |
| # Waterline extension face generation function | |
| 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 | |
| # Get envelope of model to height of face, then fuse with model and refine the shape | |
| 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) | |
| # Get top face(s) of envelope at face height | |
| rawList = list() | |
| for f in env.Faces: | |
| if Path.Geom.isRoughly(f.BoundBox.ZMin, faceHeight): | |
| rawList.append(f) | |
| # make compound and extrude downward | |
| rawComp = Part.makeCompound(rawList) | |
| rawCompExtNeg = rawComp.extrude(FreeCAD.Vector(0.0, 0.0, baseBB.ZMin - faceHeight - 1.0)) | |
| # Cut off bottom of base shape at face height | |
| topSolid = base_shape.cut(rawCompExtNeg) | |
| # Get intersection with base shape | |
| # The commented version returns waterlines that only intersects horizontal faces at same height as target face | |
| # cmn = base_shape.common(rawComp) | |
| # waterlineShape = cmn.cut(topSolid) | |
| # return waterlineShape | |
| # This version returns more of a true waterline flowing from target face | |
| 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 | |